The promotion engine is a powerful marketing instrument for the shop manager. It allows to easily create a new promotion that meets the company's requirements. However, in some cases, the shop manager needs a special or individual promotion condition that is not part of the Intershop standard. For such cases, you can create a new condition yourself that fulfills the shopping experience.
This article shows how to create a new promotion condition in a few steps and how the promotion process can be enhanced by creating user-defined parameters.
It is not necessary to copy the source code from each section. You may use the support cartridges for your own business as a blueprint:
support_bc_promotion
Contains the business logic, templates and pipelines
Download link: support_bc_promotion.zip
Our demo condition is applied in the basket context. The business requirements are to check if the basket total is greater than the configured minimum order value and if the basket is marked as recurring. If both requirements are met, the condition is true. Otherwise it is false and the discount is not applied to the basket.
This guide will teach you:
First, we use the Component Framework, see Concept - Component Framework, to declare dependencies between code artifacts.
All available conditions are loaded using the component framework. Therefore create two component files for your new condition BasketRecurringOrderCondition.
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <implementation name="BasketRecurringOrderCondition" class="com.intershop.support.promotion.condition.descriptor.RecurringOrderCondition" /> </components>
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <instance name="BasketRecurringOrderCondition" with="BasketRecurringOrderCondition" /> <fulfill requirement="conditionDescriptor" of="promotionConditionDescriptorRegistry" with="BasketRecurringOrderCondition" /> </components>
We create a new condition called RecurringOrderCondition. The requirements for this PromotionConditionDescriptor are:
A unique ID RecurringOrderCondition
Specify the condition type in our scenario it is an order condition
Set a reference to RecurringOrderConditionEvaluator, this class will be created later
Set a template references (configTemplate, configErrorTemplate, displayTemplate)
Add the condition parameters MinOrderValue, the parameter stores the minimum order value which we need later in the evaluator
package com.intershop.support.promotion.condition.descriptor; import java.util.HashMap; import com.intershop.beehive.core.capi.naming.NamingMgr; import com.intershop.component.foundation.capi.condition.Condition; import com.intershop.component.marketing.capi.promotion.condition.descriptor.PromotionConditionDescriptor; import com.intershop.support.promotion.condition.evaluator.RecurringOrderConditionEvaluator; public class RecurringOrderCondition extends PromotionConditionDescriptor { /** * ID of the condition descriptor. */ public static final String ID = "RecurringOrderCondition"; public RecurringOrderCondition() { super(ID, PromotionConditionDescriptor.CONDITION_TYPE.Order); evaluator = new RecurringOrderConditionEvaluator(); NamingMgr.injectMembers(evaluator); configTemplate = "inc/condition/RecurringOrderCondition"; configErrorTemplate = "inc/condition/RecurringOrderConditionError"; displayTemplate = "inc/condition/DisplayRecurringOrderCondition"; addConditionParameters(); } /** * Adds the parameters for <code>RecurringOrderCondition</code> to the * descriptor. * <ol> * <li>Minimum Order Value : <code>Integer</code></li> * </ol> */ private void addConditionParameters() { HashMap<String, HashMap<String, String>> paramProps0 = new HashMap<>(); HashMap<String, String> paramValidProps = new HashMap<>(); paramValidProps.put("min", "1"); paramValidProps.put("max", String.valueOf(Integer.MAX_VALUE)); paramProps0.put("GlobalValidators-intrange", paramValidProps); addParameter("MinOrderValue", "java.lang.Integer", paramProps0, null, null, "condition.error.RecurringOrderCondition.MinOrderValue"); } /** * Returns the specified minimum order value for the condition. * @param condition The condition * @return The specified minimum order value, if not set, returns * <code>null</code> */ public Integer getMinOrderValue(Condition condition) { if (condition.getConditionDescriptorID().equals(ID)) { return condition.getInteger("MinOrderValue"); } return null; } }
The creation or handling for all conditions is the same and looks like a walkthrough. It is easily possible to modify templates for the new condition that meets the business requirements.
Configures the condition parameter MinOrderValue:
<!--- TEMPLATENAME: RecurringOrderCondition.isml ---> <iscontent charset="UTF-8" compact="true" type="text/html"/> <table border="0" cellpadding="0" cellspacing="0"> <tr> <td class="fielditem2"><istext key="RecurringOrderCondition.MinOrderValue.fielditem"/></td> <td class="table_detail"><istext key="RecurringOrderCondition.MinOrderValue.table_detail"/></td> </tr> <tr> <td <isif condition="#ConditionForm:MinOrderValue:Invalid#">class="fielditem2_error"<iselse>class="fielditem2"</isif> nowrap="nowrap"><istext key="RecurringOrderCondition.MinOrderValue"/></td> <td class="table_detail"> <input type="text" maxlength="8" size="12" class="inputfield_en" name="<isprint value="#ConditionForm:MinOrderValue:QualifiedName#">" value="<isprint value="#ConditionForm:MinOrderValue:Value#">" /> </td> </tr> </table>
The display template for the configuration:
<!--- TEMPLATENAME: DisplayRecurringOrderCondition.isml ---> <iscontent charset="UTF-8" compact="true" type="text/html"> <isif condition="#NOT(isDefined(readonly) AND (readonly EQ 'true'))#"> <a href="<isprint value="#ConditionConfigurationURL#">" class="table_detail_link"><isprint value="#ThisDescriptor:Name#"/></a> <iselse> <isprint value="#ThisDescriptor:Name#"/> </isif> </br></br> <isif condition="#isDefined(Condition:MinOrderValue)#"> <istext key="RecurringOrderCondition.MinOrderValue"/> <span class="emValue"><istext key="DisplayRecurringOrderCondition.IS.link"/></span> <isprint value="#Condition:MinOrderValue#"/> </isif>
The error template displays an error if an invalid integer value is entered:
<!--- TEMPLATENAME: RecurringOrderConditionError.isml ---> <iscontent charset="UTF-8"> <isif condition="#(ConditionForm:MinOrderValue:Invalid EQ 'true')#"> <istext key="#ConditionForm:MinOrderValue:Message#"><br/> </isif>
Our templates use keys which can be localized for each language. For our demo scenario we use the en_US locale. Create the following files:
promotion_condition_en.properties
promotion_condition_auditing_en.properties
# names condition.RecurringOrderCondition.Name=Subscription # errors condition.error.RecurringOrderCondition.MinOrderValue=Please, insert a valid number, which is > 1. RecurringOrderCondition.MinOrderValue.fielditem=Condition: RecurringOrderCondition.MinOrderValue.table_detail=Subscription RecurringOrderCondition.MinOrderValue=Minimum Order Value: DisplayRecurringOrderCondition.IS.link=IS
auditing.attribute.com.intershop.component.marketing.internal.promotion.condition.RebateConditionPO.conditionDescriptorID.value.RecurringOrderCondition=Cart-Abonnement
We have two options in which context the evaluator should work. Either on the product detail page or during the basket processing.
For our created condition, we use the basket context.
Requirements to implement the evaluator are:
Check if the custom basket parameter and the minimum order value are present, otherwise return false
Check if the basket grand total < minimum order value, otherwise return false
Check if the basket is marked as recurring, otherwise return false
package com.intershop.support.promotion.condition.evaluator; import java.math.BigDecimal; import javax.inject.Inject; import com.intershop.beehive.bts.capi.orderprocess.LineItemCtnr; import com.intershop.beehive.bts.internal.orderprocess.basket.BasketPO; import com.intershop.component.application.capi.ApplicationBO; import com.intershop.component.application.capi.CurrentApplicationBOProvider; import com.intershop.component.basket.capi.BasketBO; import com.intershop.component.basket.capi.BasketBORepository; import com.intershop.component.basket.capi.extension.BasketBORecurringExtension; import com.intershop.component.foundation.capi.condition.Condition; import com.intershop.component.marketing.capi.promotion.evaluation.PromotionEvaluationContext; import com.intershop.component.marketing.capi.promotion.evaluation.PromotionEvaluationObjects; import com.intershop.component.marketing.capi.promotion.evaluation.PromotionEvaluationResult; import com.intershop.component.marketing.capi.promotion.evaluation.PromotionFilterConditionEvaluator; import com.intershop.component.marketing.internal.promotion.condition.evaluator.AbstractPromotionFilterConditionEvaluator; import com.intershop.support.promotion.condition.descriptor.RecurringOrderCondition; @SuppressWarnings("deprecation") public class RecurringOrderConditionEvaluator extends AbstractPromotionFilterConditionEvaluator implements PromotionFilterConditionEvaluator { @Inject private CurrentApplicationBOProvider currentApplicationBOProvider; private static final String BASKET = "Basket"; @Override public PromotionEvaluationResult evaluate(Condition condition, PromotionEvaluationObjects promotionEvaluationObjects, int iterationCount, PromotionEvaluationContext context, String filterConditionID, boolean filterConditionMandatory, boolean effectDiscountLevel) { switch (context.getType()) { case BASKET: return evaluateBasketCondition(condition, promotionEvaluationObjects, iterationCount, context, filterConditionID, filterConditionMandatory); } return null; } private PromotionEvaluationResult evaluateBasketCondition(Condition condition, PromotionEvaluationObjects promotionEvaluationObjects, int iterationCount, PromotionEvaluationContext context, String filterConditionID, boolean filterConditionMandatory) { PromotionEvaluationResult promotionEvaluationResult = new PromotionEvaluationResult(); promotionEvaluationResult.setSuccessful(false); // add the basket for more see Concept - Promotion Custom Parameter Passing and Cookbook - Promotions - Add Custom Parameters for the Promotion Process LineItemCtnr basket = promotionEvaluationObjects.getCustomPromotionParameter(BASKET); ApplicationBO applicationBO = currentApplicationBOProvider.get(); BasketBORepository basketBORepository = applicationBO.getRepository("BasketBORepository"); RecurringOrderCondition orderValueCondition = ((RecurringOrderCondition)context.getConditionDescriptorRegistry().getDescriptor(condition.getConditionDescriptorID())); if (orderValueCondition.getMinOrderValue(condition) == null || basket == null) { return promotionEvaluationResult; } BasketBO basketBO = basketBORepository.getBasketBO(((BasketPO)basket).getUUID()); // the basket grand total is less than the minimum order value if (basketBO.getGrandTotalGross().getValue().compareTo(BigDecimal.valueOf(orderValueCondition.getMinOrderValue(condition))) == -1) { return promotionEvaluationResult; } BasketBORecurringExtension basketBORecurringExtension = basketBO.getExtension(BasketBORecurringExtension.EXTENSION_ID); if (basketBORecurringExtension == null) { return promotionEvaluationResult; } else { if (basketBORecurringExtension.isRecurring()) { promotionEvaluationResult.setSuccessful(true); } } return promotionEvaluationResult; } }
Our new evaluator uses the method getCustomPromotionParameter(BASKET). We expect that our new condition uses a Basket for evaluation.
There are two ways to reach this goal:
Copy the pipeline ProcessBasketCalculation to your project and delete all start nodes except PreparePromotionCalculationData.
We need this start node to extend the parameter map with our current basket. The following screenshot shows the pipelet AddEntryToMap and detailed information to add the Basket to the parameter map. This is all to add a parameter for a new condition that can be handled in the context of the evaluation.
Imagine all required code artifacts were placed into the project, then the Basket parameter can be accessed by the getCustomPromotionParameter(BASKET) method invocation.
Cookbook - Extension Points | Recipe: Create an Extension Pipeline describes the required steps to create a new extension pipeline. For this guide we create a new extension SupportCustomPromotion and a new pipeline HandleCustomPromotionParameters which will be invoked if the PrepareCustomPromotionParameterextension point is executed. To keep it simple the pipeline HandleCustomPromotionParameters only contains a start and an end note. Feel free to modify them for your own purposes.
The last action is to integrate our new cartridge support_bc_promotion into existing applications or new applications. Therefore we create a new file app-extentions.component:
<?xml version="1.0" encoding="UTF-8"?> <components xmlns="http://www.intershop.de/component/2010"> <fulfill requirement="selectedCartridge" of="intershop.EnterpriseBackoffice.Cartridges" value="support_bc_promotion"/> <fulfill requirement="selectedCartridge" of="intershop.B2CBackoffice.Cartridges" value="support_bc_promotion"/> </components>
In as_responsive there is a file apps.component. Open this file and add our new cartridge.
The following lines are not complete but demonstrate the extension of the existing CartridgeListProvider:
<components xmlns="http://www.intershop.de/component/2010"> <!-- ************************************************************************************ --> <!-- * Application Type "intershop.B2CResponsive" * --> <!-- ************************************************************************************ --> <instance name="intershop.B2CResponsive.Cartridges" with="CartridgeListProvider"> <fulfill requirement="selectedCartridge" value="support_bc_promotion"/> ... </instance>
To build, deploy and test the new promotion condition, perform the following steps: