Author: nmalin
Date: Thu May 17 14:03:46 2018
New Revision: 1831783

URL: http://svn.apache.org/viewvc?rev=1831783&view=rev
Log:
Improved: Migrate product promotion condition and action rule on SOA
(OFBIZ-10370)

Currently promotion rule engine works with :
    entities ProductPromoCond et ProductPromoAction
    java linear function ProductPromoWorker.checkCondition() 
(ProductPromoWorker:910) and ProductPromoWorker.performAction 
(ProductPromoWorker:1423)
    Enumeration list to indicate on java function what piece of code to 
activate the control or action

The problem with this structure is when you want to create a new case of 
condition or action, you need to modify the framework code base.

We convert the 2 big java leaner function to service representation with one 
service by case. To realize it we introduce a relation with CustomMethod :
  ProductPromoRule --> ProductPromoCond   -> CustomMethod
                    -> ProductPromoAction -> CustomMethod
Each functions's case are converted to service with a related CustomMethod.
With this pattern now you can write your own condition rule and action rule on 
your plugin and link it on the promo engine

For backward compatibility purpose, current "PPIP" enumeration entries receive 
in enumCode data the new corresponding customMethodId reference, to ensure that 
old data works fine with the new engine
on EntityData
 <CustomMethod customMethodId="PPC_PRODUCT_AMOUNT" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondProductAmount" description="Product amount"/>
 <Enumeration enumId="PPIP_PRODUCT_AMOUNT" 
enumCode="PPC_PRODUCT_AMOUNT"/><!--link enumeration with customMethod for 
backward compatibility-->
on Java
 //for backward compatibility resolve customMethodId from enumCode
 GenericValue condEnum = 
EntityQuery.use(delegator).from("Enumeration").where("enumId", 
inputParamEnumId).cache().queryOne();
 if (condEnum != null) {
     customMethod = 
EntityQuery.use(delegator).from("CustomMethod").where("customMethodId", 
condEnum.get("enumCode")).cache().queryOne();
     if (customMethod == null) {
         Debug.logWarning("The oldest enumeration " + inputParamEnumId + " for 
promo " + productPromoCond.getPkShortValueString() + " haven't the new 
customMethod to use, please check your data or load seed data", module);
         return false;
     }
     serviceName = customMethod.getString("customMethodName");
 }

Many thanks to Ziri Khalifa and Leila Mekika for their helping work

Added:
    
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml 
  (with props)
    
ofbiz/ofbiz-framework/trunk/applications/product/groovyScripts/product/promo/
    
ofbiz/ofbiz-framework/trunk/applications/product/groovyScripts/product/promo/ProductPromoActionServices.groovy
   (with props)
    
ofbiz/ofbiz-framework/trunk/applications/product/groovyScripts/product/promo/ProductPromoCondServices.groovy
   (with props)
    ofbiz/ofbiz-framework/trunk/applications/product/groovyScripts/product/test/
    
ofbiz/ofbiz-framework/trunk/applications/product/groovyScripts/product/test/ProductPromoActionTests.groovy
   (with props)
    
ofbiz/ofbiz-framework/trunk/applications/product/groovyScripts/product/test/ProductPromoCondTests.groovy
   (with props)
    
ofbiz/ofbiz-framework/trunk/applications/product/testdef/ProductPromoTests.xml  
 (with props)
Modified:
    
ofbiz/ofbiz-framework/trunk/applications/datamodel/entitydef/product-entitymodel.xml
    ofbiz/ofbiz-framework/trunk/applications/order/servicedef/services_cart.xml
    
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartItem.java
    
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartServices.java
    
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/product/ProductPromoWorker.java
    ofbiz/ofbiz-framework/trunk/applications/product/ofbiz-component.xml
    
ofbiz/ofbiz-framework/trunk/applications/product/servicedef/services_pricepromo.xml
    
ofbiz/ofbiz-framework/trunk/applications/product/template/promo/EditProductPromoRules.ftl
    
ofbiz/ofbiz-framework/trunk/applications/product/widget/catalog/PromoScreens.xml

Modified: 
ofbiz/ofbiz-framework/trunk/applications/datamodel/entitydef/product-entitymodel.xml
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/applications/datamodel/entitydef/product-entitymodel.xml?rev=1831783&r1=1831782&r2=1831783&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/applications/datamodel/entitydef/product-entitymodel.xml
 (original)
+++ 
ofbiz/ofbiz-framework/trunk/applications/datamodel/entitydef/product-entitymodel.xml
 Thu May 17 14:03:46 2018
@@ -3480,6 +3480,7 @@ under the License.
       <field name="productPromoRuleId" type="id"></field>
       <field name="productPromoActionSeqId" type="id"></field>
       <field name="productPromoActionEnumId" type="id"></field>
+      <field name="customMethodId" type="id"/>
       <field name="orderAdjustmentTypeId" type="id"></field>
       <field name="serviceName" type="long-varchar"></field>
       <field name="quantity" type="fixed-point"></field>
@@ -3493,6 +3494,9 @@ under the License.
       <relation type="one" fk-name="PROD_PRACT_ENUM" title="Action" 
rel-entity-name="Enumeration">
         <key-map field-name="productPromoActionEnumId" 
rel-field-name="enumId"/>
       </relation>
+      <relation type="one" fk-name="PROD_PRACT_CMET" 
rel-entity-name="CustomMethod">
+        <key-map field-name="customMethodId"/>
+      </relation>
       <relation type="one" fk-name="PROD_PRACT_PR" 
rel-entity-name="ProductPromo">
         <key-map field-name="productPromoId"/>
       </relation>
@@ -3616,6 +3620,7 @@ under the License.
       <field name="productPromoId" type="id"></field>
       <field name="productPromoRuleId" type="id"></field>
       <field name="productPromoCondSeqId" type="id"></field>
+      <field name="customMethodId" type="id"/>
       <field name="inputParamEnumId" type="id"></field>
       <field name="operatorEnumId" type="id"></field>
       <field name="condValue" type="long-varchar"></field>
@@ -3630,6 +3635,9 @@ under the License.
         <key-map field-name="productPromoId"/>
         <key-map field-name="productPromoRuleId"/>
       </relation>
+      <relation type="one" fk-name="PROD_PRCOND_CMETH" 
rel-entity-name="CustomMethod">
+        <key-map field-name="customMethodId"/>
+      </relation>
       <relation type="one" fk-name="PROD_PRCOND_INENUM" title="InputParam" 
rel-entity-name="Enumeration">
         <key-map field-name="inputParamEnumId" rel-field-name="enumId"/>
       </relation>

Modified: 
ofbiz/ofbiz-framework/trunk/applications/order/servicedef/services_cart.xml
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/applications/order/servicedef/services_cart.xml?rev=1831783&r1=1831782&r2=1831783&view=diff
==============================================================================
--- ofbiz/ofbiz-framework/trunk/applications/order/servicedef/services_cart.xml 
(original)
+++ ofbiz/ofbiz-framework/trunk/applications/order/servicedef/services_cart.xml 
Thu May 17 14:03:46 2018
@@ -24,21 +24,6 @@ under the License.
     <vendor>OFBiz</vendor>
     <version>1.0</version>
 
-    <service name="interfaceProductPromoCond" engine="interface" location="" 
invoke="">
-        <attribute name="productPromoCond" 
type="org.apache.ofbiz.entity.GenericValue" mode="IN"/>
-        <attribute name="shoppingCart" 
type="org.apache.ofbiz.order.shoppingcart.ShoppingCart" mode="IN"/>
-        <attribute name="nowTimestamp" type="Timestamp" mode="IN"/>
-        <attribute name="directResult" type="Boolean" mode="OUT" 
optional="true"/>
-        <attribute name="compareBase" type="Integer" mode="OUT" 
optional="true"/>
-        <attribute name="operatorEnumId" type="String" mode="OUT" 
optional="true"/>
-    </service>
-    <service name="interfaceProductPromoAction" engine="interface" location="" 
invoke="">
-        <attribute name="productPromoAction" 
type="org.apache.ofbiz.entity.GenericValue" mode="IN"/>
-        <attribute name="shoppingCart" 
type="org.apache.ofbiz.order.shoppingcart.ShoppingCart" mode="IN"/>
-        <attribute name="nowTimestamp" type="Timestamp" mode="IN"/>
-        <attribute name="actionResultInfo" 
type="org.apache.ofbiz.order.shoppingcart.product.ProductPromoWorker$ActionResultInfo"
 mode="IN"/>
-        <attribute name="cartItemModifyException" 
type="org.apache.ofbiz.order.shoppingcart.CartItemModifyException" mode="OUT" 
optional="true"/>
-    </service>
     <service name="assignItemShipGroup" engine="java" auth="false"
             
location="org.apache.ofbiz.order.shoppingcart.ShoppingCartServices" 
invoke="assignItemShipGroup">
         <description>Assign a ShoppingCartItem -> Quantity to a ship 
group</description>

Modified: 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartItem.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartItem.java?rev=1831783&r1=1831782&r2=1831783&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartItem.java
 (original)
+++ 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartItem.java
 Thu May 17 14:03:46 2018
@@ -2034,7 +2034,10 @@ public class ShoppingCartItem implements
 
     /** Returns the total line price. */
     public BigDecimal getItemSubTotal(BigDecimal quantity) {
-          return 
getBasePrice().multiply(quantity).multiply(getRentalAdjustment()).add(getOtherAdjustments());
+        BigDecimal basePrice = getBasePrice();
+        BigDecimal rentalAdj = getRentalAdjustment();
+        BigDecimal otherAdj = getOtherAdjustments();
+        return basePrice.multiply(quantity).multiply(rentalAdj).add(otherAdj);
     }
 
     public BigDecimal getItemSubTotal() {

Modified: 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartServices.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartServices.java?rev=1831783&r1=1831782&r2=1831783&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartServices.java
 (original)
+++ 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/ShoppingCartServices.java
 Thu May 17 14:03:46 2018
@@ -562,11 +562,11 @@ public class ShoppingCartServices {
                 // set the PO number on the cart
                 cart.setPoNumber(item.getString("correspondingPoId"));
 
-                // get all item adjustments EXCEPT tax adjustments
+                // get all item adjustments EXCEPT tax and promo adjustments 
that will be recalculate
                 List<GenericValue> itemAdjustments = 
orh.getOrderItemAdjustments(item);
                 if (itemAdjustments != null) {
                     for (GenericValue itemAdjustment : itemAdjustments) {
-                        if (!isTaxAdjustment(itemAdjustment)) {
+                        if (!isTaxAdjustment(itemAdjustment) && 
!isPromoAdjustment(itemAdjustment)) {
                             cartItem.addAdjustment(itemAdjustment);
                         }
                     }
@@ -969,9 +969,12 @@ public class ShoppingCartServices {
 
     private static boolean isTaxAdjustment(GenericValue cartAdj) {
         String adjType = cartAdj.getString("orderAdjustmentTypeId");
-
         return "SALES_TAX".equals(adjType) || "VAT_TAX".equals(adjType) || 
"VAT_PRICE_CORRECT".equals(adjType);
     }
+    private static boolean isPromoAdjustment(GenericValue cartAdj) {
+        String adjType = cartAdj.getString("orderAdjustmentTypeId");
+        return "PROMOTION_ADJUSTMENT".equals(adjType);
+    }
 
     public static Map<String, Object>loadCartFromShoppingList(DispatchContext 
dctx, Map<String, Object> context) {
         LocalDispatcher dispatcher = dctx.getDispatcher();

Modified: 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/product/ProductPromoWorker.java
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/product/ProductPromoWorker.java?rev=1831783&r1=1831782&r2=1831783&view=diff
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/product/ProductPromoWorker.java
 (original)
+++ 
ofbiz/ofbiz-framework/trunk/applications/order/src/main/java/org/apache/ofbiz/order/shoppingcart/product/ProductPromoWorker.java
 Thu May 17 14:03:46 2018
@@ -61,12 +61,7 @@ import org.apache.ofbiz.product.product.
 import org.apache.ofbiz.product.store.ProductStoreWorker;
 import org.apache.ofbiz.service.GenericServiceException;
 import org.apache.ofbiz.service.LocalDispatcher;
-import org.apache.ofbiz.service.ModelService;
 import org.apache.ofbiz.service.ServiceUtil;
-import org.apache.ofbiz.service.calendar.RecurrenceInfo;
-import org.apache.ofbiz.service.calendar.RecurrenceInfoException;
-
-import com.ibm.icu.util.Calendar;
 
 /**
  * ProductPromoWorker - Worker class for catalog/product promotion related 
functionality
@@ -930,378 +925,69 @@ public final class ProductPromoWorker {
         }
         Integer compareBase = null;
 
-        if ("PPIP_SERVICE".equals(inputParamEnumId)) {
+        //resolve the service name to use
+        String serviceName = null;
+        GenericValue customMethod = 
productPromoCond.getRelatedOne("CustomMethod", true);
+        if (customMethod != null) {
+            serviceName = customMethod.getString("customMethodName");
+        } else {
+            if ("PPIP_SERVICE".equals(inputParamEnumId)) {
+                serviceName = condValue;
+            } else {
+                //for backward compatibility resolve customMethodId from 
enumCode
+                GenericValue condEnum = 
EntityQuery.use(delegator).from("Enumeration").where("enumId", 
inputParamEnumId).cache().queryOne();
+                if (condEnum != null) {
+                    customMethod = 
EntityQuery.use(delegator).from("CustomMethod").where("customMethodId", 
condEnum.get("enumCode")).cache().queryOne();
+                    if (customMethod == null) {
+                        Debug.logWarning("The oldest enumeration " + 
inputParamEnumId + " for promo " + productPromoCond.getPkShortValueString()
+                                + " haven't the new customMethod to use, 
please check your data or load seed data", module);
+                        return false;
+                    }
+                    serviceName = customMethod.getString("customMethodName");
+                }
+            }
+        }
+        
+        if (serviceName != null) {
             Map<String, Object> serviceCtx = UtilMisc.<String, 
Object>toMap("productPromoCond", productPromoCond, "shoppingCart", cart, 
"nowTimestamp", nowTimestamp);
-            Map<String, Object> condResult;
+            Map<String, Object> condResult = null;
             try {
-                condResult = dispatcher.runSync(condValue, serviceCtx);
+                condResult = dispatcher.runSync(serviceName, serviceCtx);
             } catch (GenericServiceException e) {
-                Debug.logError(e, "Fatal error calling promo condition check 
service [" + condValue + "]", module);
-                return false;
-            }
-            if (ServiceUtil.isError(condResult)) {
-                Debug.logError("Error calling calling promo condition check 
service [" + condValue + "]", module);
+                Debug.logWarning("Failed to execute productPromoCond service " 
+ serviceName + " for promo " + productPromoCond.getPkShortValueString() + " 
throw " + e.toString(), module);
                 return false;
             }
-            Boolean directResult = (Boolean) condResult.get("directResult");
-            if (directResult != null) {
-                return directResult.booleanValue();
-            }
             compareBase = (Integer) condResult.get("compareBase");
             if (condResult.containsKey("operatorEnumId")) {
                 operatorEnumId = (String) condResult.get("operatorEnumId");
             }
-        } else if ("PPIP_PRODUCT_AMOUNT".equals(inputParamEnumId)) {
-            // for this type of promo force the operatorEnumId = PPC_EQ, 
effectively ignore that setting because the comparison is implied in the code
-            operatorEnumId = "PPC_EQ";
-
-            // this type of condition requires items involved to not be 
involved in any other quantity consuming cond/action, and does not pro-rate the 
price, just uses the base price
-            BigDecimal amountNeeded = BigDecimal.ZERO;
-            if (UtilValidate.isNotEmpty(condValue)) {
-                amountNeeded = new BigDecimal(condValue);
-            }
-
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleCondProductIds(productPromoCond, delegator, 
nowTimestamp);
-
-            List<ShoppingCartItem> lineOrderedByBasePriceList = 
cart.getLineListOrderedByBasePrice(false);
-            Iterator<ShoppingCartItem> lineOrderedByBasePriceIter = 
lineOrderedByBasePriceList.iterator();
-            while (amountNeeded.compareTo(BigDecimal.ZERO) > 0 && 
lineOrderedByBasePriceIter.hasNext()) {
-                ShoppingCartItem cartItem = lineOrderedByBasePriceIter.next();
-                // only include if it is in the productId Set for this check 
and if it is not a Promo (GWP) item
-                GenericValue product = cartItem.getProduct();
-                String parentProductId = cartItem.getParentProductId();
-                boolean passedItemConds = 
checkConditionsForItem(productPromoCond, cart, cartItem, delegator, dispatcher, 
nowTimestamp);
-                if (passedItemConds && !cartItem.getIsPromo() &&
-                        (productIds.contains(cartItem.getProductId()) || 
(parentProductId != null && productIds.contains(parentProductId))) &&
-                        (product == null || 
!"N".equals(product.getString("includeInPromotions")))) {
-
-                    BigDecimal basePrice = cartItem.getBasePrice();
-                    // get a rough price, round it up to an integer
-                    BigDecimal quantityNeeded = amountNeeded.divide(basePrice, 
generalRounding).setScale(0, RoundingMode.CEILING);
-
-                    // reduce amount still needed to qualify for promo 
(amountNeeded)
-                    BigDecimal quantity = 
cartItem.addPromoQuantityCandidateUse(quantityNeeded, productPromoCond, false);
-                    // get pro-rated amount based on discount
-                    amountNeeded = 
amountNeeded.subtract(quantity.multiply(basePrice));
-                }
-            }
-
-            // if amountNeeded > 0 then the promo condition failed, so remove 
candidate promo uses and increment the promoQuantityUsed to restore it
-            if (amountNeeded.compareTo(BigDecimal.ZERO) > 0) {
-                // failed, reset the entire rule, ie including all other 
conditions that might have been done before
-                
cart.resetPromoRuleUse(productPromoCond.getString("productPromoId"), 
productPromoCond.getString("productPromoRuleId"));
-                compareBase = Integer.valueOf(-1);
-            } else {
-                // we got it, the conditions are in place...
-                compareBase = Integer.valueOf(0);
-                // NOTE: don't confirm promo rule use here, wait until actions 
are complete for the rule to do that
-            }
-        } else if ("PPIP_PRODUCT_TOTAL".equals(inputParamEnumId)) {
-            // this type of condition allows items involved to be involved in 
other quantity consuming cond/action, and does pro-rate the price
-            if (UtilValidate.isNotEmpty(condValue)) {
-                BigDecimal amountNeeded = new BigDecimal(condValue);
-                BigDecimal amountAvailable = BigDecimal.ZERO;
-
-                Set<String> productIds = 
ProductPromoWorker.getPromoRuleCondProductIds(productPromoCond, delegator, 
nowTimestamp);
-
-                List<ShoppingCartItem> lineOrderedByBasePriceList = 
cart.getLineListOrderedByBasePrice(false);
-                for (ShoppingCartItem cartItem : lineOrderedByBasePriceList) {
-                    // only include if it is in the productId Set for this 
check and if it is not a Promo (GWP) item
-                    GenericValue product = cartItem.getProduct();
-                    String parentProductId = cartItem.getParentProductId();
-                    boolean passedItemConds = 
checkConditionsForItem(productPromoCond, cart, cartItem, delegator, dispatcher, 
nowTimestamp);
-                    if (passedItemConds && !cartItem.getIsPromo() &&
-                            (productIds.contains(cartItem.getProductId()) || 
(parentProductId != null && productIds.contains(parentProductId))) &&
-                            (product == null || 
!"N".equals(product.getString("includeInPromotions")))) {
-
-                        // just count the entire sub-total of the item
-                        amountAvailable = 
amountAvailable.add(cartItem.getItemSubTotal());
-                    }
-                }
-
-                compareBase = 
Integer.valueOf(amountAvailable.compareTo(amountNeeded));
-            }
-        } else if ("PPIP_PRODUCT_QUANT".equals(inputParamEnumId)) {
-            if (operatorEnumId == null) {
-                // if the operator is not specified in the condition, then 
assume as default PPC_EQ (for backward compatibility)
-                operatorEnumId = "PPC_EQ";
-            }
-            BigDecimal quantityNeeded = BigDecimal.ONE;
-            if (UtilValidate.isNotEmpty(condValue)) {
-                quantityNeeded = new BigDecimal(condValue);
-            }
-
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleCondProductIds(productPromoCond, delegator, 
nowTimestamp);
-
-            List<ShoppingCartItem> lineOrderedByBasePriceList = 
cart.getLineListOrderedByBasePrice(false);
-            Iterator<ShoppingCartItem> lineOrderedByBasePriceIter = 
lineOrderedByBasePriceList.iterator();
-            while (quantityNeeded.compareTo(BigDecimal.ZERO) > 0 && 
lineOrderedByBasePriceIter.hasNext()) {
-                ShoppingCartItem cartItem = lineOrderedByBasePriceIter.next();
-                // only include if it is in the productId Set for this check 
and if it is not a Promo (GWP) item
-                GenericValue product = cartItem.getProduct();
-                String parentProductId = cartItem.getParentProductId();
-                boolean passedItemConds = 
checkConditionsForItem(productPromoCond, cart, cartItem, delegator, dispatcher, 
nowTimestamp);
-                if (passedItemConds && !cartItem.getIsPromo() &&
-                        (productIds.contains(cartItem.getProductId()) || 
(parentProductId != null && productIds.contains(parentProductId))) &&
-                        (product == null || 
!"N".equals(product.getString("includeInPromotions")))) {
-                    // reduce quantity still needed to qualify for promo 
(quantityNeeded)
-                    quantityNeeded = 
quantityNeeded.subtract(cartItem.addPromoQuantityCandidateUse(quantityNeeded, 
productPromoCond, !"PPC_EQ".equals(operatorEnumId)));
-                }
-            }
-
-            // if quantityNeeded > 0 then the promo condition failed, so 
remove candidate promo uses and increment the promoQuantityUsed to restore it
-            if (quantityNeeded.compareTo(BigDecimal.ZERO) > 0) {
-                // failed, reset the entire rule, ie including all other 
conditions that might have been done before
-                
cart.resetPromoRuleUse(productPromoCond.getString("productPromoId"), 
productPromoCond.getString("productPromoRuleId"));
-                compareBase = Integer.valueOf(-1);
-            } else {
-                // we got it, the conditions are in place...
-                compareBase = Integer.valueOf(0);
-                // NOTE: don't confirm rpomo rule use here, wait until actions 
are complete for the rule to do that
-            }
-
-        } else if ("PPIP_NEW_ACCT".equals(inputParamEnumId)) {
-            if (UtilValidate.isNotEmpty(condValue)) {
-                BigDecimal acctDays = 
cart.getPartyDaysSinceCreated(nowTimestamp);
-                if (acctDays == null) {
-                    // condition always fails if we don't know how many days 
since account created
-                    return false;
-                }
-                compareBase = acctDays.compareTo(new BigDecimal(condValue));
-            }
-        } else if ("PPIP_PARTY_ID".equals(inputParamEnumId)) {
-            if (partyId != null && UtilValidate.isNotEmpty(condValue)) {
-                compareBase = Integer.valueOf(partyId.compareTo(condValue));
-            } else {
-                compareBase = Integer.valueOf(1);
-            }
-        } else if ("PPIP_PARTY_GRP_MEM".equals(inputParamEnumId)) {
-            if (UtilValidate.isEmpty(partyId) || 
UtilValidate.isEmpty(condValue)) {
-                compareBase = Integer.valueOf(1);
-            } else {
-                String groupPartyId = condValue;
-                if (partyId.equals(groupPartyId)) {
-                    compareBase = Integer.valueOf(0);
-                } else {
-                    // look for PartyRelationship with 
partyRelationshipTypeId=GROUP_ROLLUP, the partyIdTo is the group member, so the 
partyIdFrom is the groupPartyId
-                    // and from/thru date within range
-                    List<GenericValue>  partyRelationshipList = 
EntityQuery.use(delegator).from("PartyRelationship").where("partyIdFrom", 
groupPartyId, "partyIdTo", partyId, "partyRelationshipTypeId", 
"GROUP_ROLLUP").cache(true).filterByDate().queryList();
-
-                    if (UtilValidate.isNotEmpty(partyRelationshipList)) {
-                        compareBase = Integer.valueOf(0);
-                    } else {
-                        compareBase = 
Integer.valueOf(checkConditionPartyHierarchy(delegator, nowTimestamp, 
groupPartyId, partyId));
-                    }
-                }
-            }
-        } else if ("PPIP_PARTY_CLASS".equals(inputParamEnumId)) {
-            if (UtilValidate.isEmpty(partyId) || 
UtilValidate.isEmpty(condValue)) {
-                compareBase = Integer.valueOf(1);
-            } else {
-                String partyClassificationGroupId = condValue;
-                // find any PartyClassification
-                // and from/thru date within range
-                List<GenericValue> partyClassificationList = 
EntityQuery.use(delegator).from("PartyClassification").where("partyId", 
partyId, "partyClassificationGroupId", 
partyClassificationGroupId).cache(true).filterByDate().queryList();
-                // then 0 (equals), otherwise 1 (not equals)
-                if (UtilValidate.isNotEmpty(partyClassificationList)) {
-                    compareBase = Integer.valueOf(0);
-                } else {
-                    compareBase = Integer.valueOf(1);
-                }
-            }
-        } else if ("PPIP_ROLE_TYPE".equals(inputParamEnumId)) {
-            if (partyId != null && UtilValidate.isNotEmpty(condValue)) {
-                // if a PartyRole exists for this partyId and the specified 
roleTypeId
-                GenericValue partyRole = 
EntityQuery.use(delegator).from("PartyRole").where("partyId", partyId, 
"roleTypeId", condValue).cache(true).queryOne();
-                // then 0 (equals), otherwise 1 (not equals)
-                if (partyRole != null) {
-                    compareBase = Integer.valueOf(0);
+            
+            if (Debug.verboseOn()) Debug.logVerbose("Condition compare done, 
compareBase=" + compareBase, module);
+            if (compareBase != null) {
+                int compare = compareBase.intValue();
+                if ("PPC_EQ".equals(operatorEnumId)) {
+                    if (compare == 0) return true;
+                } else if ("PPC_NEQ".equals(operatorEnumId)) {
+                    if (compare != 0) return true;
+                } else if ("PPC_LT".equals(operatorEnumId)) {
+                    if (compare < 0) return true;
+                } else if ("PPC_LTE".equals(operatorEnumId)) {
+                    if (compare <= 0) return true;
+                } else if ("PPC_GT".equals(operatorEnumId)) {
+                    if (compare > 0) return true;
+                } else if ("PPC_GTE".equals(operatorEnumId)) {
+                    if (compare >= 0) return true;
                 } else {
-                    compareBase = Integer.valueOf(1);
-                }
-            } else {
-                compareBase = Integer.valueOf(1);
-            }
-        } else if ("PPIP_GEO_ID".equals(inputParamEnumId)) {
-            compareBase = Integer.valueOf(1);
-            GenericValue shippingAddress = cart.getShippingAddress();
-            if (UtilValidate.isNotEmpty(condValue) && shippingAddress != null) 
{
-                if(condValue.equals(shippingAddress.getString("countryGeoId")) 
|| condValue.equals(shippingAddress.getString("countyGeoId"))
-                        || 
condValue.equals(shippingAddress.getString("postalCodeGeoId")) || 
condValue.equals(shippingAddress.getString("stateProvinceGeoId"))) {
-                    compareBase = Integer.valueOf(0);
-                } else {
-                    List<GenericValue> geoAssocList = 
EntityQuery.use(delegator).from("GeoAssoc").where("geoIdTo", 
condValue).queryList();
-                    for (GenericValue geo : geoAssocList) {
-                        
if(geo.get("geoId").equals(shippingAddress.getString("countryGeoId")) || 
geo.get("geoId").equals(shippingAddress.getString("countyGeoId")) || 
geo.get("geoId").equals(shippingAddress.getString("postalCodeGeoId"))
-                            || 
condValue.equals(shippingAddress.getString("stateProvinceGeoId"))) {
-                            compareBase = Integer.valueOf(0);
-                            break;
-                        }
-                    }
-                }
-            }
-        } else if ("PPIP_ORDER_TOTAL".equals(inputParamEnumId)) {
-            if (UtilValidate.isNotEmpty(condValue)) {
-                BigDecimal orderSubTotal = cart.getSubTotalForPromotions();
-                if (Debug.verboseOn()) Debug.logVerbose("Doing order total 
compare: orderSubTotal=" + orderSubTotal, module);
-                compareBase = Integer.valueOf(orderSubTotal.compareTo(new 
BigDecimal(condValue)));
-            }
-        } else if ("PPIP_ORST_HIST".equals(inputParamEnumId)) {
-            // description="Order sub-total X in last Y Months"
-            if (partyId != null && userLogin != null && 
UtilValidate.isNotEmpty(condValue)) {
-                // call the getOrderedSummaryInformation service to get the 
sub-total
-                int monthsToInclude = 12;
-                if (otherValue != null) {
-                    monthsToInclude = Integer.parseInt(otherValue);
-                }
-                Map<String, Object> serviceIn = UtilMisc.<String, 
Object>toMap("partyId", partyId, "roleTypeId", "PLACING_CUSTOMER", 
"orderTypeId", "SALES_ORDER", "statusId", "ORDER_COMPLETED", "monthsToInclude", 
Integer.valueOf(monthsToInclude), "userLogin", userLogin);
-                try {
-                    Map<String, Object> result = 
dispatcher.runSync("getOrderedSummaryInformation", serviceIn);
-                    if (ServiceUtil.isError(result)) {
-                        Debug.logError("Error calling 
getOrderedSummaryInformation service for the PPIP_ORST_HIST ProductPromo 
condition input value: " + ServiceUtil.getErrorMessage(result), module);
-                        return false;
-                    } else {
-                        BigDecimal orderSubTotal = (BigDecimal) 
result.get("totalSubRemainingAmount");
-                        BigDecimal orderSubTotalAndCartSubTotal = 
orderSubTotal.add(cart.getSubTotal()); 
-                        if (Debug.verboseOn()) Debug.logVerbose("Doing order 
history sub-total compare: orderSubTotal=" + orderSubTotal + ", for the last " 
+ monthsToInclude + " months.", module);
-                        compareBase = 
Integer.valueOf(orderSubTotalAndCartSubTotal.compareTo(new 
BigDecimal(condValue)));
-                    }
-                } catch (GenericServiceException e) {
-                    Debug.logError(e, "Error getting order history sub-total 
in the getOrderedSummaryInformation service, evaluating condition to false.", 
module);
-                    return false;
-                }
-            } else {
-                return false;
-            }
-        } else if ("PPIP_ORST_YEAR".equals(inputParamEnumId)) {
-            // description="Order sub-total X since beginning of current year"
-            if (partyId != null && userLogin != null && 
UtilValidate.isNotEmpty(condValue)) {
-                // call the getOrderedSummaryInformation service to get the 
sub-total
-                Calendar calendar = Calendar.getInstance();
-                calendar.setTime(nowTimestamp);
-                int monthsToInclude = calendar.get(Calendar.MONTH) + 1;
-                Map<String, Object> serviceIn = UtilMisc.<String, 
Object>toMap("partyId", partyId,
-                        "roleTypeId", "PLACING_CUSTOMER",
-                        "orderTypeId", "SALES_ORDER",
-                        "statusId", "ORDER_COMPLETED",
-                        "monthsToInclude", Integer.valueOf(monthsToInclude),
-                        "userLogin", userLogin);
-                try {
-                    Map<String, Object> result = 
dispatcher.runSync("getOrderedSummaryInformation", serviceIn);
-                    if (ServiceUtil.isError(result)) {
-                        Debug.logError("Error calling 
getOrderedSummaryInformation service for the PPIP_ORST_YEAR ProductPromo 
condition input value: " + ServiceUtil.getErrorMessage(result), module);
-                        return false;
-                    } else {
-                        BigDecimal orderSubTotal = (BigDecimal) 
result.get("totalSubRemainingAmount");
-                        if (Debug.verboseOn()) Debug.logVerbose("Doing order 
history sub-total compare: orderSubTotal=" + orderSubTotal + ", for the last " 
+ monthsToInclude + " months.", module);
-                        compareBase = 
Integer.valueOf(orderSubTotal.compareTo(new BigDecimal((condValue))));
-                    }
-                } catch (GenericServiceException e) {
-                    Debug.logError(e, "Error getting order history sub-total 
in the getOrderedSummaryInformation service, evaluating condition to false.", 
module);
-                    return false;
-                }
-            }
-        } else if ("PPIP_ORST_LAST_YEAR".equals(inputParamEnumId)) {
-            // description="Order sub-total X since beginning of last year"
-            if (partyId != null && userLogin != null && 
UtilValidate.isNotEmpty(condValue)) {
-                // call the getOrderedSummaryInformation service to get the 
sub-total
-
-                Calendar calendar = Calendar.getInstance();
-                calendar.setTime(nowTimestamp);
-                int lastYear = calendar.get(Calendar.YEAR) - 1;
-                Calendar fromDateCalendar = Calendar.getInstance();
-                fromDateCalendar.set(lastYear, 0, 0, 0, 0);
-                Timestamp fromDate = new 
Timestamp(fromDateCalendar.getTime().getTime());
-                Calendar thruDateCalendar = Calendar.getInstance();
-                thruDateCalendar.set(lastYear, 12, 0, 0, 0);
-                Timestamp thruDate = new 
Timestamp(thruDateCalendar.getTime().getTime());
-                Map<String, Object> serviceIn = UtilMisc.toMap("partyId", 
partyId,
-                        "roleTypeId", "PLACING_CUSTOMER",
-                        "orderTypeId", "SALES_ORDER",
-                        "statusId", "ORDER_COMPLETED",
-                        "fromDate", fromDate,
-                        "thruDate", thruDate,
-                        "userLogin", userLogin);
-                try {
-                    Map<String, Object> result = 
dispatcher.runSync("getOrderedSummaryInformation", serviceIn);
-                    if (ServiceUtil.isError(result)) {
-                        Debug.logError("Error calling 
getOrderedSummaryInformation service for the PPIP_ORST_LAST_YEAR ProductPromo 
condition input value: " + ServiceUtil.getErrorMessage(result), module);
-                        return false;
-                    } else {
-                        Double orderSubTotal = (Double) 
result.get("totalSubRemainingAmount");
-                        if (Debug.verboseOn()) Debug.logVerbose("Doing order 
history sub-total compare: orderSubTotal=" + orderSubTotal + ", for last 
year.", module);
-                        compareBase = 
Integer.valueOf(orderSubTotal.compareTo(Double.valueOf(condValue)));
-                    }
-                } catch (GenericServiceException e) {
-                    Debug.logError(e, "Error getting order history sub-total 
in the getOrderedSummaryInformation service, evaluating condition to false.", 
module);
+                    
Debug.logWarning(UtilProperties.getMessage(resource_error,"OrderAnUnSupportedProductPromoCondCondition",
 UtilMisc.toMap("operatorEnumId",operatorEnumId) , cart.getLocale()), module);
                     return false;
                 }
-            } else {
-                return false;
-            }
-        } else if ("PPIP_RECURRENCE".equals(inputParamEnumId)) {
-            if (UtilValidate.isNotEmpty(condValue)) {
-                compareBase = Integer.valueOf(1);
-                GenericValue recurrenceInfo = 
EntityQuery.use(delegator).from("RecurrenceInfo").where("recurrenceInfoId", 
condValue).cache().queryOne();
-                if (recurrenceInfo != null) {
-                    RecurrenceInfo recurrence = null;
-                    try {
-                        recurrence = new RecurrenceInfo(recurrenceInfo);
-                    } catch (RecurrenceInfoException e) {
-                        Debug.logError(e, module);
-                    }
-
-                    // check the current recurrence
-                    if (recurrence != null) {
-                        if (recurrence.isValidCurrent()) {
-                            compareBase = Integer.valueOf(0);
-                        }
-                    }
-                }
-            }
-        } else if ("PPIP_ORDER_SHIPTOTAL".equals(inputParamEnumId) && 
shippingMethod.equals(cart.getShipmentMethodTypeId()) && 
carrierPartyId.equals(cart.getCarrierPartyId())) {
-            if (UtilValidate.isNotEmpty(condValue)) {
-                BigDecimal orderTotalShipping = cart.getTotalShipping();
-                if (Debug.verboseOn()) { Debug.logVerbose("Doing order total 
Shipping compare: ordertotalShipping=" + orderTotalShipping, module); }
-                compareBase = orderTotalShipping.compareTo(new 
BigDecimal(condValue));
-            }
-        } else if ("PPIP_LPMUP_AMT".equals(inputParamEnumId)) {
-            // does nothing on order level, only checked on item level, so 
ignore by always considering passed
-            return true;
-        } else if ("PPIP_LPMUP_PER".equals(inputParamEnumId)) {
-            // does nothing on order level, only checked on item level, so 
ignore by always considering passed
-            return true;
-        } else {
-            
Debug.logWarning(UtilProperties.getMessage(resource_error,"OrderAnUnSupportedProductPromoCondInputParameterLhs",
 
UtilMisc.toMap("inputParamEnumId",productPromoCond.getString("inputParamEnumId")),
 cart.getLocale()), module);
-            return false;
-        }
-
-        if (Debug.verboseOn()) Debug.logVerbose("Condition compare done, 
compareBase=" + compareBase, module);
-
-        if (compareBase != null) {
-            int compare = compareBase.intValue();
-            if ("PPC_EQ".equals(operatorEnumId)) {
-                if (compare == 0) return true;
-            } else if ("PPC_NEQ".equals(operatorEnumId)) {
-                if (compare != 0) return true;
-            } else if ("PPC_LT".equals(operatorEnumId)) {
-                if (compare < 0) return true;
-            } else if ("PPC_LTE".equals(operatorEnumId)) {
-                if (compare <= 0) return true;
-            } else if ("PPC_GT".equals(operatorEnumId)) {
-                if (compare > 0) return true;
-            } else if ("PPC_GTE".equals(operatorEnumId)) {
-                if (compare >= 0) return true;
-            } else {
-                
Debug.logWarning(UtilProperties.getMessage(resource_error,"OrderAnUnSupportedProductPromoCondCondition",
 UtilMisc.toMap("operatorEnumId",operatorEnumId) , cart.getLocale()), module);
-                return false;
             }
         }
         // default to not meeting the condition
         return false;
     }
 
-    private static boolean checkConditionsForItem(GenericValue 
productPromoActionOrCond, ShoppingCart cart, ShoppingCartItem cartItem, 
Delegator delegator, LocalDispatcher dispatcher, Timestamp nowTimestamp) throws 
GenericEntityException {
+    public static boolean checkConditionsForItem(GenericValue 
productPromoActionOrCond, ShoppingCart cart, ShoppingCartItem cartItem, 
Delegator delegator, LocalDispatcher dispatcher, Timestamp nowTimestamp) throws 
GenericEntityException {
         GenericValue productPromoRule = 
productPromoActionOrCond.getRelatedOne("ProductPromoRule", true);
 
         List<GenericValue> productPromoConds = 
EntityQuery.use(delegator).from("ProductPromoCond").where("productPromoId", 
productPromoRule.get("productPromoId")).orderBy("productPromoCondSeqId").cache(true).queryList();
@@ -1315,7 +1001,7 @@ public final class ProductPromoWorker {
         return true;
     }
 
-    private static boolean checkConditionForItem(GenericValue 
productPromoCond, ShoppingCart cart, ShoppingCartItem cartItem, Delegator 
delegator, LocalDispatcher dispatcher, Timestamp nowTimestamp) throws 
GenericEntityException {
+    public static boolean checkConditionForItem(GenericValue productPromoCond, 
ShoppingCart cart, ShoppingCartItem cartItem, Delegator delegator, 
LocalDispatcher dispatcher, Timestamp nowTimestamp) throws 
GenericEntityException {
         String condValue = productPromoCond.getString("condValue");
         String inputParamEnumId = 
productPromoCond.getString("inputParamEnumId");
         String operatorEnumId = productPromoCond.getString("operatorEnumId");
@@ -1393,7 +1079,7 @@ public final class ProductPromoWorker {
         return true;
     }
 
-    private static int checkConditionPartyHierarchy(Delegator delegator, 
Timestamp nowTimestamp, String groupPartyId, String partyId) throws 
GenericEntityException{
+    public static int checkConditionPartyHierarchy(Delegator delegator, 
Timestamp nowTimestamp, String groupPartyId, String partyId) throws 
GenericEntityException{
         List<GenericValue> partyRelationshipList = 
EntityQuery.use(delegator).from("PartyRelationship").where("partyIdTo", 
partyId, "partyRelationshipTypeId", 
"GROUP_ROLLUP").cache(true).filterByDate(nowTimestamp).queryList();
         for (GenericValue genericValue : partyRelationshipList) {
             String partyIdFrom = (String)genericValue.get("partyIdFrom");
@@ -1421,12 +1107,29 @@ public final class ProductPromoWorker {
     }
 
     public static void performAction(ActionResultInfo actionResultInfo, 
GenericValue productPromoAction, ShoppingCart cart, Delegator delegator, 
LocalDispatcher dispatcher, Timestamp nowTimestamp) throws 
GenericEntityException, CartItemModifyException {
-
         String productPromoActionEnumId = 
productPromoAction.getString("productPromoActionEnumId");
+        String serviceName = null;
+        GenericValue customMethod = 
productPromoAction.getRelatedOne("CustomMethod", true);
+        
+        if (customMethod != null) {
+            serviceName = customMethod.getString("customMethodName");
+        } else {
+            if ("PROMO_SERVICE".equals(productPromoActionEnumId)) {
+                serviceName = productPromoAction.getString("serviceName");
+            } else {
+                //for backware compatibility resolve customMethodId from 
enumCode
+                GenericValue condEnum = 
EntityQuery.use(delegator).from("Enumeration").where("enumId", 
productPromoActionEnumId).cache().queryOne();
+                if (condEnum != null) {
+                    customMethod = 
EntityQuery.use(delegator).from("CustomMethod").where("customMethodId", 
condEnum.get("enumCode")).cache().queryOne();
+                    if (customMethod != null) {
+                        serviceName = 
customMethod.getString("customMethodName");
+                    }
+                }
+            }
+        } 
 
-        if ("PROMO_SERVICE".equals(productPromoActionEnumId)) {
+        if (serviceName != null) {
             Map<String, Object> serviceCtx = UtilMisc.<String, 
Object>toMap("productPromoAction", productPromoAction, "shoppingCart", cart, 
"nowTimestamp", nowTimestamp, "actionResultInfo", actionResultInfo);
-            String serviceName = productPromoAction.getString("serviceName");
             Map<String, Object> actionResult;
             try {
                 actionResult = dispatcher.runSync(serviceName, serviceCtx);
@@ -1438,399 +1141,11 @@ public final class ProductPromoWorker {
                 Debug.logError("Error calling promo action service [" + 
serviceName + "], result is: " + actionResult, module);
                 throw new 
CartItemModifyException(ServiceUtil.getErrorMessage(actionResult));
             }
+            actionResultInfo = (ActionResultInfo) 
actionResult.get("actionResultInfo");
             CartItemModifyException cartItemModifyException = 
(CartItemModifyException) actionResult.get("cartItemModifyException");
             if (cartItemModifyException != null) {
                 throw cartItemModifyException;
             }
-        } else if ("PROMO_GWP".equals(productPromoActionEnumId)) {
-            String productStoreId = cart.getProductStoreId();
-
-            // the code was in there for this, so even though I don't think we 
want to restrict this, just adding this flag to make it easy to change; could 
make option dynamic, but now implied by the use limit
-            boolean allowMultipleGwp = true;
-
-            Integer itemLoc = findPromoItem(productPromoAction, cart);
-            if (!allowMultipleGwp && itemLoc != null) {
-                if (Debug.verboseOn()) {
-                    Debug.logVerbose("Not adding promo item, already there; 
action: " + productPromoAction, module);
-                }
-                actionResultInfo.ranAction = false;
-            } else {
-                BigDecimal quantity;
-                if (productPromoAction.get("quantity") != null) {
-                    quantity = productPromoAction.getBigDecimal("quantity");
-                } else {
-                    if ("Y".equals(productPromoAction.get("useCartQuantity"))) 
{
-                        quantity = BigDecimal.ZERO;
-                        List<ShoppingCartItem> used = getCartItemsUsed(cart, 
productPromoAction);
-                        for (ShoppingCartItem item : used) {
-                            BigDecimal available = 
item.getPromoQuantityAvailable();
-                            quantity = 
quantity.add(available).add(item.getPromoQuantityCandidateUseActionAndAllConds(productPromoAction));
-                            item.addPromoQuantityCandidateUse(available, 
productPromoAction, false);
-                        }
-                    } else {
-                        quantity = BigDecimal.ZERO;
-                    }
-                }
-
-                List<String> optionProductIds = new LinkedList<>();
-                String productId = productPromoAction.getString("productId");
-
-                GenericValue product = null;
-                if (UtilValidate.isNotEmpty(productId)) {
-                    product = 
EntityQuery.use(delegator).from("Product").where("productId", 
productId).cache().queryOne();
-                    if (product == null) {
-                        String errMsg = "GWP Product not found with ID [" + 
productId + "] for ProductPromoAction [" + 
productPromoAction.get("productPromoId") + ":" + 
productPromoAction.get("productPromoRuleId") + ":" + 
productPromoAction.get("productPromoActionSeqId") + "]";
-                        Debug.logError(errMsg, module);
-                        throw new CartItemModifyException(errMsg);
-                    }
-                    if ("Y".equals(product.getString("isVirtual"))) {
-                        List<GenericValue> productAssocs = 
EntityUtil.filterByDate(product.getRelated("MainProductAssoc", 
UtilMisc.toMap("productAssocTypeId", "PRODUCT_VARIANT"), 
UtilMisc.toList("sequenceNum"), true));
-                        for (GenericValue productAssoc : productAssocs) {
-                            
optionProductIds.add(productAssoc.getString("productIdTo"));
-                        }
-                        productId = null;
-                        product = null;
-                    } else {
-                        // check inventory on this product, make sure it is 
available before going on
-                        //NOTE: even though the store may not require 
inventory for purchase, we will always require inventory for gifts
-                        try {
-                            // get the quantity in cart for inventory check
-                            BigDecimal quantityAlreadyInCart = BigDecimal.ZERO;
-                            List<ShoppingCartItem> matchingItems = 
cart.findAllCartItems(productId);
-                            for (ShoppingCartItem item : matchingItems) {
-                                quantityAlreadyInCart = 
quantityAlreadyInCart.add(item.getQuantity());
-                            }
-                            Map<String, Object> invReqResult = 
dispatcher.runSync("isStoreInventoryAvailable", UtilMisc.<String, 
Object>toMap("productStoreId", productStoreId, "productId", productId, 
"product", product, "quantity", quantity.add(quantityAlreadyInCart)));
-                            if (ServiceUtil.isError(invReqResult)) {
-                                Debug.logError("Error calling 
isStoreInventoryAvailable service, result is: " + invReqResult, module);
-                                throw new 
CartItemModifyException(ServiceUtil.getErrorMessage(invReqResult));
-                            } else if 
(!"Y".equals(invReqResult.get("available"))) {
-                                
Debug.logWarning(UtilProperties.getMessage(resource_error,"OrderNotApplyingGwpBecauseProductIdIsOutOfStockForProductPromoAction",
 UtilMisc.toMap("productId", productId, "productPromoAction", 
productPromoAction), cart.getLocale()), module);
-                                productId = null;
-                                product = null;
-                            }
-                        } catch (GenericServiceException e) {
-                            String errMsg = "Fatal error calling inventory 
checking services: " + e.toString();
-                            Debug.logError(e, errMsg, module);
-                            throw new CartItemModifyException(errMsg);
-                        }
-                    }
-                }
-
-                // support multiple gift options if products are attached to 
the action, or if the productId on the action is a virtual product
-                Set<String> productIds = 
ProductPromoWorker.getPromoRuleActionProductIds(productPromoAction, delegator, 
nowTimestamp);
-                optionProductIds.addAll(productIds);
-
-                // make sure these optionProducts have inventory...
-                Iterator<String> optionProductIdIter = 
optionProductIds.iterator();
-                while (optionProductIdIter.hasNext()) {
-                    String optionProductId = optionProductIdIter.next();
-
-                    try {
-                        // get the quantity in cart for inventory check
-                        BigDecimal quantityAlreadyInCart = BigDecimal.ZERO;
-                        List<ShoppingCartItem> matchingItems = 
cart.findAllCartItems(optionProductId);
-                        for (ShoppingCartItem item : matchingItems) {
-                            quantityAlreadyInCart = 
quantityAlreadyInCart.add(item.getQuantity());
-                        }
-
-                        Map<String, Object> invReqResult = 
dispatcher.runSync("isStoreInventoryAvailable", UtilMisc.<String, 
Object>toMap("productStoreId", productStoreId, "productId", optionProductId, 
"product", product, "quantity", quantity.add(quantityAlreadyInCart)));
-                        if (ServiceUtil.isError(invReqResult)) {
-                            Debug.logError("Error calling 
isStoreInventoryAvailable service, result is: " + invReqResult, module);
-                            throw new 
CartItemModifyException(ServiceUtil.getErrorMessage(invReqResult));
-                        } else if (!"Y".equals(invReqResult.get("available"))) 
{
-                            optionProductIdIter.remove();
-                        }
-                    } catch (GenericServiceException e) {
-                        String errMsg = "Fatal error calling inventory 
checking services: " + e.toString();
-                        Debug.logError(e, errMsg, module);
-                        throw new CartItemModifyException(errMsg);
-                    }
-                }
-
-                // check to see if any desired productIds have been selected 
for this promo action
-                String alternateGwpProductId = 
cart.getDesiredAlternateGiftByAction(productPromoAction.getPrimaryKey());
-                if (UtilValidate.isNotEmpty(alternateGwpProductId)) {
-                    // also check to make sure this isn't a spoofed ID 
somehow, check to see if it is in the Set
-                    if (optionProductIds.contains(alternateGwpProductId)) {
-                        if (UtilValidate.isNotEmpty(productId)) {
-                            optionProductIds.add(productId);
-                        }
-                        optionProductIds.remove(alternateGwpProductId);
-                        productId = alternateGwpProductId;
-                        product = 
EntityQuery.use(delegator).from("Product").where("productId", 
productId).cache().queryOne();
-                    } else {
-                        
Debug.logWarning(UtilProperties.getMessage(resource_error,"OrderAnAlternateGwpProductIdWasInPlaceButWasEitherNotValidOrIsNoLongerInStockForId",
 UtilMisc.toMap("alternateGwpProductId",alternateGwpProductId), 
cart.getLocale()), module);
-                    }
-                }
-
-                // if product is null, get one from the productIds set
-                if (product == null && optionProductIds.size() > 0) {
-                    // get the first from an iterator and remove it since it 
will be the current one
-                    Iterator<String> optionProductIdTempIter = 
optionProductIds.iterator();
-                    productId = optionProductIdTempIter.next();
-                    optionProductIdTempIter.remove();
-                    product = 
EntityQuery.use(delegator).from("Product").where("productId", 
productId).cache().queryOne();
-                }
-
-                if (product == null) {
-                    // no product found to add as GWP, just return
-                    return;
-                }
-
-                // pass null for cartLocation to add to end of cart, pass 
false for doPromotions to avoid infinite recursion
-                ShoppingCartItem gwpItem = null;
-                try {
-                    // just leave the prodCatalogId null, this line won't be 
associated with a catalog
-                    gwpItem = ShoppingCartItem.makeItem(null, product, null, 
quantity, null, null, null, null, null, null, null, null, null, null, null, 
null, dispatcher, cart, Boolean.FALSE, Boolean.TRUE, null, Boolean.FALSE, 
Boolean.FALSE);
-                    if (optionProductIds.size() > 0) {
-                        
gwpItem.setAlternativeOptionProductIds(optionProductIds);
-                    } else {
-                        gwpItem.setAlternativeOptionProductIds(null);
-                    }
-                } catch (CartItemModifyException e) {
-                    Debug.logError(e.getMessage(), module);
-                    throw e;
-                }
-
-                BigDecimal discountAmount = 
quantity.multiply(gwpItem.getBasePrice()).negate();
-
-                doOrderItemPromoAction(productPromoAction, gwpItem, 
discountAmount, "amount", delegator);
-
-                // set promo after create; note that to setQuantity we must 
clear this flag, setQuantity, then re-set the flag
-                gwpItem.setIsPromo(true);
-                if (Debug.verboseOn()) {
-                    Debug.logVerbose("gwpItem adjustments: " + 
gwpItem.getAdjustments(), module);
-                }
-
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = discountAmount;
-            }
-        } else if ("PROMO_FREE_SHIPPING".equals(productPromoActionEnumId)) {
-            // this may look a bit funny: on each pass all rules that do free 
shipping will set their own rule id for it,
-            // and on unapply if the promo and rule ids are the same then it 
will clear it; essentially on any pass
-            // through the promos and rules if any free shipping should be 
there, it will be there
-            cart.addFreeShippingProductPromoAction(productPromoAction);
-            // don't consider this as a cart change?
-            actionResultInfo.ranAction = true;
-            // should probably set the totalDiscountAmount to something, but 
we have no idea what it will be, so leave at 0, will still get run
-        } else if ("PROMO_PROD_DISC".equals(productPromoActionEnumId)) {
-            BigDecimal quantityDesired = productPromoAction.get("quantity") == 
null ? BigDecimal.ONE : productPromoAction.getBigDecimal("quantity");
-            BigDecimal startingQuantity = quantityDesired;
-            BigDecimal discountAmountTotal = BigDecimal.ZERO;
-
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleActionProductIds(productPromoAction, delegator, 
nowTimestamp);
-
-            List<ShoppingCartItem> lineOrderedByBasePriceList = 
cart.getLineListOrderedByBasePrice(false);
-            Iterator<ShoppingCartItem> lineOrderedByBasePriceIter = 
lineOrderedByBasePriceList.iterator();
-            while (quantityDesired.compareTo(BigDecimal.ZERO) > 0 && 
lineOrderedByBasePriceIter.hasNext()) {
-                ShoppingCartItem cartItem = lineOrderedByBasePriceIter.next();
-                // only include if it is in the productId Set for this check 
and if it is not a Promo (GWP) item
-                GenericValue product = cartItem.getProduct();
-                String parentProductId = cartItem.getParentProductId();
-                boolean passedItemConds = 
checkConditionsForItem(productPromoAction, cart, cartItem, delegator, 
dispatcher, nowTimestamp);
-                if (passedItemConds && !cartItem.getIsPromo() &&
-                        (productIds.contains(cartItem.getProductId()) || 
(parentProductId != null && productIds.contains(parentProductId))) &&
-                        (product == null || 
!"N".equals(product.getString("includeInPromotions")))) {
-                    // reduce quantity still needed to qualify for promo 
(quantityNeeded)
-                    BigDecimal quantityUsed = 
cartItem.addPromoQuantityCandidateUse(quantityDesired, productPromoAction, 
false);
-                    if (quantityUsed.compareTo(BigDecimal.ZERO) > 0) {
-                        quantityDesired = 
quantityDesired.subtract(quantityUsed);
-
-                        // create an adjustment and add it to the cartItem 
that implements the promotion action
-                        BigDecimal percentModifier = 
productPromoAction.get("amount") == null ? BigDecimal.ZERO : 
productPromoAction.getBigDecimal("amount").movePointLeft(2);
-                        BigDecimal lineAmount = 
quantityUsed.multiply(cartItem.getBasePrice()).multiply(cartItem.getRentalAdjustment());
-                        BigDecimal discountAmount = 
lineAmount.multiply(percentModifier).negate();
-                        discountAmountTotal = 
discountAmountTotal.add(discountAmount);
-                        // not doing this any more, now distributing among 
conditions and actions (see call below): 
doOrderItemPromoAction(productPromoAction, cartItem, discountAmount, "amount", 
delegator);
-                    }
-                }
-            }
-
-            if (quantityDesired.compareTo(startingQuantity) == 0 || 
quantityDesired.compareTo(BigDecimal.ZERO) > 0) {
-                // couldn't find any (or enough) cart items to give a discount 
to, don't consider action run
-                actionResultInfo.ranAction = false;
-                // clear out any action uses for this so they don't become 
part of anything else
-                
cart.resetPromoRuleUse(productPromoAction.getString("productPromoId"), 
productPromoAction.getString("productPromoRuleId"));
-            } else {
-                BigDecimal totalAmount = getCartItemsUsedTotalAmount(cart, 
productPromoAction);
-                if (Debug.verboseOn()) {
-                    Debug.logVerbose("Applying promo [" + 
productPromoAction.getPrimaryKey() + "]\n totalAmount=" + totalAmount + ", 
discountAmountTotal=" + discountAmountTotal, module);
-                }
-                distributeDiscountAmount(discountAmountTotal, totalAmount, 
getCartItemsUsed(cart, productPromoAction), productPromoAction, delegator);
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = discountAmountTotal;
-                actionResultInfo.quantityLeftInAction = quantityDesired;
-            }
-        } else if ("PROMO_PROD_AMDISC".equals(productPromoActionEnumId)) {
-            BigDecimal quantityDesired = productPromoAction.get("quantity") == 
null ? BigDecimal.ONE : productPromoAction.getBigDecimal("quantity");
-            BigDecimal startingQuantity = quantityDesired;
-            BigDecimal discountAmountTotal = BigDecimal.ZERO;
-
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleActionProductIds(productPromoAction, delegator, 
nowTimestamp);
-
-            List<ShoppingCartItem> lineOrderedByBasePriceList = 
cart.getLineListOrderedByBasePrice(false);
-            Iterator<ShoppingCartItem> lineOrderedByBasePriceIter = 
lineOrderedByBasePriceList.iterator();
-            while (quantityDesired.compareTo(BigDecimal.ZERO) > 0 && 
lineOrderedByBasePriceIter.hasNext()) {
-                ShoppingCartItem cartItem = lineOrderedByBasePriceIter.next();
-                // only include if it is in the productId Set for this check 
and if it is not a Promo (GWP) item
-                String parentProductId = cartItem.getParentProductId();
-                GenericValue product = cartItem.getProduct();
-                boolean passedItemConds = 
checkConditionsForItem(productPromoAction, cart, cartItem, delegator, 
dispatcher, nowTimestamp);
-                if (passedItemConds && !cartItem.getIsPromo() &&
-                        (productIds.contains(cartItem.getProductId()) || 
(parentProductId != null && productIds.contains(parentProductId))) &&
-                        (product == null || 
!"N".equals(product.getString("includeInPromotions")))) {
-                    // reduce quantity still needed to qualify for promo 
(quantityNeeded)
-                    BigDecimal quantityUsed = 
cartItem.addPromoQuantityCandidateUse(quantityDesired, productPromoAction, 
false);
-                    quantityDesired = quantityDesired.subtract(quantityUsed);
-
-                    // create an adjustment and add it to the cartItem that 
implements the promotion action
-                    BigDecimal discount = productPromoAction.get("amount") == 
null ? BigDecimal.ZERO : productPromoAction.getBigDecimal("amount");
-                    // don't allow the discount to be greater than the price
-                    if 
(discount.compareTo(cartItem.getBasePrice().multiply(cartItem.getRentalAdjustment()))
 > 0) {
-                        discount = 
cartItem.getBasePrice().multiply(cartItem.getRentalAdjustment());
-                    }
-                    BigDecimal discountAmount = 
quantityUsed.multiply(discount).negate();
-                    discountAmountTotal = 
discountAmountTotal.add(discountAmount);
-                    // not doing this any more, now distributing among 
conditions and actions (see call below): 
doOrderItemPromoAction(productPromoAction, cartItem, discountAmount, "amount", 
delegator);
-                }
-            }
-
-            if (quantityDesired.compareTo(startingQuantity) == 0) {
-                // couldn't find any cart items to give a discount to, don't 
consider action run
-                actionResultInfo.ranAction = false;
-            } else {
-                BigDecimal totalAmount = getCartItemsUsedTotalAmount(cart, 
productPromoAction);
-                if (Debug.verboseOn()) {
-                    Debug.logVerbose("Applying promo [" + 
productPromoAction.getPrimaryKey() + "]\n totalAmount=" + totalAmount + ", 
discountAmountTotal=" + discountAmountTotal, module);
-                }
-                distributeDiscountAmount(discountAmountTotal, totalAmount, 
getCartItemsUsed(cart, productPromoAction), productPromoAction, delegator);
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = discountAmountTotal;
-                actionResultInfo.quantityLeftInAction = quantityDesired;
-            }
-        } else if ("PROMO_PROD_PRICE".equals(productPromoActionEnumId)) {
-            // with this we want the set of used items to be one price, so 
total the price for all used items, subtract the amount we want them to cost, 
and create an adjustment for what is left
-            BigDecimal quantityDesired = productPromoAction.get("quantity") == 
null ? BigDecimal.ONE : productPromoAction.getBigDecimal("quantity");
-            BigDecimal desiredAmount = productPromoAction.get("amount") == 
null ? BigDecimal.ZERO : productPromoAction.getBigDecimal("amount");
-            BigDecimal totalAmount = BigDecimal.ZERO;
-
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleActionProductIds(productPromoAction, delegator, 
nowTimestamp);
-
-            List<ShoppingCartItem> cartItemsUsed = new LinkedList<>();
-            List<ShoppingCartItem> lineOrderedByBasePriceList = 
cart.getLineListOrderedByBasePrice(false);
-            Iterator<ShoppingCartItem> lineOrderedByBasePriceIter = 
lineOrderedByBasePriceList.iterator();
-            while (quantityDesired.compareTo(BigDecimal.ZERO) > 0 && 
lineOrderedByBasePriceIter.hasNext()) {
-                ShoppingCartItem cartItem = lineOrderedByBasePriceIter.next();
-                // only include if it is in the productId Set for this check 
and if it is not a Promo (GWP) item
-                String parentProductId = cartItem.getParentProductId();
-                GenericValue product = cartItem.getProduct();
-                boolean passedItemConds = 
checkConditionsForItem(productPromoAction, cart, cartItem, delegator, 
dispatcher, nowTimestamp);
-                if (passedItemConds && !cartItem.getIsPromo() && 
(productIds.contains(cartItem.getProductId()) || (parentProductId != null && 
productIds.contains(parentProductId))) &&
-                        (product == null || 
!"N".equals(product.getString("includeInPromotions")))) {
-                    // reduce quantity still needed to qualify for promo 
(quantityNeeded)
-                    BigDecimal quantityUsed = 
cartItem.addPromoQuantityCandidateUse(quantityDesired, productPromoAction, 
false);
-                    if (quantityUsed.compareTo(BigDecimal.ZERO) > 0) {
-                        quantityDesired = 
quantityDesired.subtract(quantityUsed);
-                        totalAmount = 
totalAmount.add(quantityUsed.multiply(cartItem.getBasePrice()).multiply(cartItem.getRentalAdjustment()));
-                        cartItemsUsed.add(cartItem);
-                    }
-                }
-            }
-
-            if (totalAmount.compareTo(desiredAmount) > 0 && 
quantityDesired.compareTo(BigDecimal.ZERO) == 0) {
-                BigDecimal discountAmountTotal = 
totalAmount.subtract(desiredAmount).negate();
-                distributeDiscountAmount(discountAmountTotal, totalAmount, 
cartItemsUsed, productPromoAction, delegator);
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = discountAmountTotal;
-                // no use setting the quantityLeftInAction because that does 
not apply for buy X for $Y type promotions, it is all or nothing
-            } else {
-                actionResultInfo.ranAction = false;
-                // clear out any action uses for this so they don't become 
part of anything else
-                
cart.resetPromoRuleUse(productPromoAction.getString("productPromoId"), 
productPromoAction.getString("productPromoRuleId"));
-            }
-        } else if ("PROMO_ORDER_PERCENT".equals(productPromoActionEnumId)) {
-            BigDecimal percentage = (productPromoAction.get("amount") == null 
? BigDecimal.ZERO : 
(productPromoAction.getBigDecimal("amount").movePointLeft(2))).negate();
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleActionProductIds(productPromoAction, delegator, 
nowTimestamp);
-            BigDecimal amount;
-            if (productIds.isEmpty()) {
-                amount = cart.getSubTotalForPromotions().multiply(percentage);
-            } else {
-                amount = 
cart.getSubTotalForPromotions(productIds).multiply(percentage);
-            }
-            if (amount.compareTo(BigDecimal.ZERO) != 0) {
-                doOrderPromoAction(productPromoAction, cart, amount, "amount", 
delegator);
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = amount;
-            }
-        } else if ("PROMO_ORDER_AMOUNT".equals(productPromoActionEnumId)) {
-            BigDecimal amount = (productPromoAction.get("amount") == null ? 
BigDecimal.ZERO : productPromoAction.getBigDecimal("amount")).negate();
-            // if amount is greater than the order sub total, set equal to 
order sub total, this normally wouldn't happen because there should be a 
condition that the order total be above a certain amount, but just in case...
-            BigDecimal subTotal = cart.getSubTotalForPromotions();
-            if (amount.negate().compareTo(subTotal) > 0) {
-                amount = subTotal.negate();
-            }
-            if (amount.compareTo(BigDecimal.ZERO) != 0) {
-                doOrderPromoAction(productPromoAction, cart, amount, "amount", 
delegator);
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = amount;
-            }
-        } else if ("PROMO_PROD_SPPRC".equals(productPromoActionEnumId)) {
-            // if there are productIds associated with the action then 
restrict to those productIds, otherwise apply for all products
-            Set<String> productIds = 
ProductPromoWorker.getPromoRuleActionProductIds(productPromoAction, delegator, 
nowTimestamp);
-
-            // go through the cart items and for each product that has a 
specialPromoPrice use that price
-            for (ShoppingCartItem cartItem : cart.items()) {
-                String itemProductId = cartItem.getProductId();
-                if (UtilValidate.isEmpty(itemProductId)) {
-                    continue;
-                }
-
-                if (productIds.size() > 0 && 
!productIds.contains(itemProductId)) {
-                    continue;
-                }
-
-                if (cartItem.getSpecialPromoPrice() == null) {
-                    continue;
-                }
-
-                // get difference between basePrice and specialPromoPrice and 
adjust for that
-                BigDecimal difference = 
cartItem.getBasePrice().multiply(cartItem.getRentalAdjustment()).subtract(cartItem.getSpecialPromoPrice()).negate();
-
-                if (difference.compareTo(BigDecimal.ZERO) != 0) {
-                    BigDecimal quantityUsed = 
cartItem.addPromoQuantityCandidateUse(cartItem.getQuantity(), 
productPromoAction, false);
-                    if (quantityUsed.compareTo(BigDecimal.ZERO) > 0) {
-                        BigDecimal amount = difference.multiply(quantityUsed);
-                        doOrderItemPromoAction(productPromoAction, cartItem, 
amount, "amount", delegator);
-                        actionResultInfo.ranAction = true;
-                        actionResultInfo.totalDiscountAmount = amount;
-                    }
-                }
-            }
-        } else if ("PROMO_SHIP_CHARGE".equals(productPromoActionEnumId)) {
-            BigDecimal percentage = (productPromoAction.get("amount") == null 
? BigDecimal.ZERO : 
(productPromoAction.getBigDecimal("amount").movePointLeft(2))).negate();
-            BigDecimal amount = cart.getTotalShipping().multiply(percentage);
-            if (amount.compareTo(BigDecimal.ZERO) != 0) {
-                int existingOrderPromoIndex = 
cart.getAdjustmentPromoIndex(productPromoAction.getString("productPromoId"));
-                if (existingOrderPromoIndex != -1 && 
cart.getAdjustment(existingOrderPromoIndex).getBigDecimal("amount").compareTo(amount)
 == 0) {
-                        actionResultInfo.ranAction = false;  // already ran, 
no need to repeat
-                } else {
-                    if (existingOrderPromoIndex != -1 && 
cart.getAdjustment(existingOrderPromoIndex).getBigDecimal("amount").compareTo(amount)
 != 0) {
-                        cart.removeAdjustment(existingOrderPromoIndex);
-                    }
-                    doOrderPromoAction(productPromoAction, cart, amount, 
"amount", delegator);
-                    actionResultInfo.ranAction = true;
-                    actionResultInfo.totalDiscountAmount = amount;
-                }
-            }
-        } else if ("PROMO_TAX_PERCENT".equals(productPromoActionEnumId)) {
-            BigDecimal percentage = (productPromoAction.get("amount") == null 
? BigDecimal.ZERO : 
(productPromoAction.getBigDecimal("amount").movePointLeft(2))).negate();
-            BigDecimal amount = cart.getTotalSalesTax().multiply(percentage);
-            if (amount.compareTo(BigDecimal.ZERO) != 0) {
-                doOrderPromoAction(productPromoAction, cart, amount, "amount", 
delegator);
-                actionResultInfo.ranAction = true;
-                actionResultInfo.totalDiscountAmount = amount;
-            }
         } else {
             Debug.logError("An un-supported productPromoActionType was used: " 
+ productPromoActionEnumId + ", not performing any action", module);
             actionResultInfo.ranAction = false;
@@ -1844,7 +1159,7 @@ public final class ProductPromoWorker {
         }
     }
 
-    private static List<ShoppingCartItem> getCartItemsUsed(ShoppingCart cart, 
GenericValue productPromoAction) {
+    public static List<ShoppingCartItem> getCartItemsUsed(ShoppingCart cart, 
GenericValue productPromoAction) {
         List<ShoppingCartItem> cartItemsUsed = new LinkedList<>();
         for (ShoppingCartItem cartItem : cart) {
             BigDecimal quantityUsed = 
cartItem.getPromoQuantityCandidateUseActionAndAllConds(productPromoAction);
@@ -1855,7 +1170,7 @@ public final class ProductPromoWorker {
         return cartItemsUsed;
     }
 
-    private static BigDecimal getCartItemsUsedTotalAmount(ShoppingCart cart, 
GenericValue productPromoAction) {
+    public static BigDecimal getCartItemsUsedTotalAmount(ShoppingCart cart, 
GenericValue productPromoAction) {
         BigDecimal totalAmount = BigDecimal.ZERO;
         for (ShoppingCartItem cartItem : cart) {
             BigDecimal quantityUsed = 
cartItem.getPromoQuantityCandidateUseActionAndAllConds(productPromoAction);
@@ -1866,7 +1181,7 @@ public final class ProductPromoWorker {
         return totalAmount;
     }
 
-    private static void distributeDiscountAmount(BigDecimal 
discountAmountTotal, BigDecimal totalAmount, List<ShoppingCartItem> 
cartItemsUsed, GenericValue productPromoAction, Delegator delegator) {
+    public static void distributeDiscountAmount(BigDecimal 
discountAmountTotal, BigDecimal totalAmount, List<ShoppingCartItem> 
cartItemsUsed, GenericValue productPromoAction, Delegator delegator) {
         BigDecimal discountAmount = discountAmountTotal;
         // distribute the discount evenly weighted according to price over the 
order items that the individual quantities came from; avoids a number of issues 
with tax/shipping calc, inclusion in the sub-total for other promotions, etc
         Iterator<ShoppingCartItem> cartItemsUsedIter = 
cartItemsUsed.iterator();
@@ -1889,7 +1204,7 @@ public final class ProductPromoWorker {
         // this is the old way that causes problems: 
doOrderPromoAction(productPromoAction, cart, discountAmount, "amount", 
delegator);
     }
 
-    private static Integer findPromoItem(GenericValue productPromoAction, 
ShoppingCart cart) {
+    public static Integer findPromoItem(GenericValue productPromoAction, 
ShoppingCart cart) {
         List<ShoppingCartItem> cartItems = cart.items();
 
         for (int i = 0; i < cartItems.size(); i++) {

Added: 
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml
URL: 
http://svn.apache.org/viewvc/ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml?rev=1831783&view=auto
==============================================================================
--- 
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml 
(added)
+++ 
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml 
Thu May 17 14:03:46 2018
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<!-- FreeMarker template for DHL ShipIT Services. If the DHL ShipIt API ever 
changes its schema, we only need to change this file. -->
+
+<entity-engine-xml>
+
+    <CustomMethodType customMethodTypeId="PRODUCT_PROMO" description=""/>
+    <CustomMethodType customMethodTypeId="PRODUCT_PROMO_COND" 
parentTypeId="PRODUCT_PROMO" description=""/>
+    <CustomMethodType customMethodTypeId="PRODUCT_PROMO_ACTION" 
parentTypeId="PRODUCT_PROMO" description=""/>
+
+    <!-- CustomMethods type Condition -->
+    <CustomMethod customMethodId="PPC_PRODUCT_AMOUNT" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondProductAmount" description="Product amount"/>
+    <Enumeration enumId="PPIP_PRODUCT_AMOUNT" 
enumCode="PPC_PRODUCT_AMOUNT"/><!--link enumeration with customMethod for 
backware compatibility-->
+    <CustomMethod customMethodId="PPC_PRODUCT_TOTAL" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondProductTotal" description="Product Total"/>
+    <Enumeration enumId="PPIP_PRODUCT_TOTAL" enumCode="PPC_PRODUCT_TOTAL"/>
+    <CustomMethod customMethodId="PPC_PRODUCT_QUANT" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondProductQuant" description="X Quantity of 
Product"/>
+    <Enumeration enumId="PPIP_PRODUCT_QUANT" enumCode="PPC_PRODUCT_QUANT"/>
+    <CustomMethod customMethodId="PPC_NEW_ACCT" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondNewACCT" description="Account Days Since 
Created"/>
+    <Enumeration enumId="PPIP_NEW_ACCT" enumCode="PPC_NEW_ACCT"/>
+    <CustomMethod customMethodId="PPC_PARTY_ID" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondPartyID" description="Party"/>
+    <Enumeration enumId="PPIP_PARTY_ID" enumCode="PPC_PARTY_ID"/>
+    <CustomMethod customMethodId="PPC_PARTY_GRP_MEM" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondPartyGM" description="Party group member"/>
+    <Enumeration enumId="PPIP_PARTY_GRP_MEM" enumCode="PPC_PARTY_GRP_MEM"/>
+    <CustomMethod customMethodId="PPC_PARTY_CLASS" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondPartyClass" description="Party 
classification"/>
+    <Enumeration enumId="PPIP_PARTY_CLASS" enumCode="PPC_PARTY_CLASS"/>
+    <CustomMethod customMethodId="PPC_ROLE_TYPE" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondRoleType" description="Role type"/>
+    <Enumeration enumId="PPIP_ROLE_TYPE" enumCode="PPC_ROLE_TYPE"/>
+    <CustomMethod customMethodId="PPC_GEO_ID" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondGeoID" description="Shipping destination"/>
+    <Enumeration enumId="PPIP_GEO_ID" enumCode="PPC_GEO_ID"/>
+    <CustomMethod customMethodId="PPC_ORDER_TOTAL" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondOrderTotal" description="Cart sub-total"/>
+    <Enumeration enumId="PPIP_ORDER_TOTAL" enumCode="PPC_ORDER_TOTAL"/>
+    <CustomMethod customMethodId="PPC_ORST_HIST" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondOrderHist" description="Order sub-total X in 
last Y Months"/>
+    <Enumeration enumId="PPIP_ORST_HIST" enumCode="PPC_ORST_HIST"/>
+    <CustomMethod customMethodId="PPC_ORST_YEAR" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondOrderYear" description="Order sub-total X 
since beginning of current year"/>
+    <Enumeration enumId="PPIP_ORST_YEAR" enumCode="PPC_ORST_YEAR"/>
+    <CustomMethod customMethodId="PPC_ORST_LAST_YEAR" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondOrderLastYear" description="Order sub-total X 
last year"/>
+    <Enumeration enumId="PPIP_ORST_LAST_YEAR" enumCode="PPC_ORST_LAST_YEAR"/>
+    <CustomMethod customMethodId="PPC_RECURRENCE" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondPromoRecurrence" description="Promotion 
Recurrence"/>
+    <Enumeration enumId="PPIP_RECURRENCE" enumCode="PPC_RECURRENCE"/>
+    <CustomMethod customMethodId="PPC_ORDER_SHIPTOTAL" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondOrderShipTotal" description="Shipping total"/>
+    <Enumeration enumId="PPIP_ORDER_SHIPTOTAL" enumCode="PPC_ORDER_SHIPTOTAL"/>
+    <CustomMethod customMethodId="PPC_LPMUP_AMT" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondListPriceMinAmount" description="List Price 
minus Unit Price (Amount)"/>
+    <Enumeration enumId="PPIP_LPMUP_AMT" enumCode="PPC_LPMUP_AMT"/>
+    <CustomMethod customMethodId="PPC_LPMUP_PER" 
customMethodTypeId="PRODUCT_PROMO_COND" 
customMethodName="productPromoCondListPriceMinPercent" description="List Price 
minus Unit Price (Percent)"/>
+    <Enumeration enumId="PPIP_LPMUP_PER" enumCode="PPC_LPMUP_PER"/>
+
+    <!-- CustomMethods type Action -->
+    <CustomMethod customMethodId="PPA_GWP" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActGiftGWP" description="gift with purchase"/>
+    <Enumeration enumId="PROMO_GWP" enumCode="PPA_GWP"/>
+    <CustomMethod customMethodId="PPA_FREE_SHIPPING" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActFreeShip" description="free shipping"/>
+    <Enumeration enumId="PROMO_FREE_SHIPPING" enumCode="PPA_FREE_SHIPPING"/>
+    <CustomMethod customMethodId="PPA_PROD_DISC" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActProdDISC" description="X Product for Y% 
Discount"/>
+    <Enumeration enumId="PROMO_PROD_DISC" enumCode="PPA_PROD_DISC"/>
+    <CustomMethod customMethodId="PPA_PROD_AMDISC" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActProdAMDISC" description="X Product for Y 
Discount"/>
+    <Enumeration enumId="PROMO_PROD_AMDISC" enumCode="PPA_PROD_AMDISC"/>
+    <CustomMethod customMethodId="PPA_PROD_PRICE" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActProdPrice" description="X Product for Y 
Price"/>
+    <Enumeration enumId="PROMO_PROD_PRICE" enumCode="PPA_PROD_PRICE"/>
+    <CustomMethod customMethodId="PPA_ORDER_PERCENT" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActOrderPercent" description="Order Percent 
Discount"/>
+    <Enumeration enumId="PROMO_ORDER_PERCENT" enumCode="PPA_ORDER_PERCENT"/>
+    <CustomMethod customMethodId="PPA_ORDER_AMOUNT" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActOrderAmount" description="Order Amount Flat"/>
+    <Enumeration enumId="PROMO_ORDER_AMOUNT" enumCode="PPA_ORDER_AMOUNT"/>
+    <CustomMethod customMethodId="PPA_PROD_SPPRC" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActProdSpecialPrice" description="Product for 
[Special Promo] Price"/>
+    <Enumeration enumId="PROMO_PROD_SPPRC" enumCode="PPA_PROD_SPPRC"/>
+    <CustomMethod customMethodId="PPA_TAX_PERCENT" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActTaxPercent" description="Tax % Discount"/>
+    <Enumeration enumId="PROMO_TAX_PERCENT" enumCode="PPA_TAX_PERCENT"/>
+    <CustomMethod customMethodId="PPA_SHIP_CHARGE" 
customMethodTypeId="PRODUCT_PROMO_ACTION" 
customMethodName="productPromoActShipCharge" description="shipping charge"/>
+    <Enumeration enumId="PROMO_SHIP_CHARGE" enumCode="PPA_SHIP_CHARGE"/>
+
+</entity-engine-xml>
+
+

Propchange: 
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: 
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml
------------------------------------------------------------------------------
    svn:keywords = Date Rev Author URL Id

Propchange: 
ofbiz/ofbiz-framework/trunk/applications/product/data/PromoCustomMethodData.xml
------------------------------------------------------------------------------
    svn:mime-type = text/xml


Reply via email to