How to create a Discount Rule extension to Liferay Commerce - DEV24 - (part 1)

DISCLAIMER: This blog  post has Spanish and English version.

Hello everybody, like I promised on Dev24, here we are in the Blog Post with the complete step by step from start to finish. Here you can learn how to create an extension “Discount Rule Type” on Liferay Commerce, with a configuration option. 

This is a link if you missed the session “Forget Amazon, help your users to buy with Alexa from Liferay Commerce (thanks to its headless APIs)” at Dev24 conference where we gave the introduction.

At this moment Liferay Commerce extensions don’t have BLADE templates and, because of that I will explain all project scaffolding here .

If you understand all the scaffolding needed, you will be able to build a lot of extensions for Liferay Portal 7.X and Liferay Commerce

For the extension we will create a new rule discount based on the age of the buyer.

Step 1, creating our New Discount Type extension for Commerce:

1.  Using Blade CLI, we can start creating our Liferay Workspace if you don’t already have one for Liferay 7.3.

blade init -v 7.3 liferay-workspace-7-3

2.  Into your “Liferay Workspace” we’ll create a custom module (discount module) 

blade create -t api -p com.liferay.commerce.discount.rule.by.age -c CommerceDiscountRuleByAge commerce-discount-rule-by-age 

3.  Now you should have this structure in your project

4. Open a gradle.properties with your favorite editor, uncomment and change the property value from “liferay.workspace.home.dir=bundle” to the path where your bundle with Liferay Commerce is installed (ej.: “liferay.workspace.home.dir=/Users/roselainedefaria/Liferay/Source/bundles”). This setup will help us to make the module deployment automatic.

Remember: we started creating a Liferay Workspace for 7.3 version, so our bundle needs to be Liferay 7.3 (for me and in the moment I wrote this Blog post, this bundle was built from master source code, cloned from Liferay Github, after built with a command “ant all” ).If you want to use another version, start again and pay attention to number version of dependencies on build.gradle file, which will probably need to be changed. 

5.  Now, rename the package from “com.liferay.commerce.discount.rule.by.age.api” to “com.liferay.commerce.discount.rule.by.age” to eliminate the “api” which “blade CLI” generates automatically (because the template “api” was used).

6.  In “bnd.bnd” file eliminate the “Export-Package: com.liferay.commerce.discount.rule.by.age.api” line.

7.  Open the folder “resources” (i.e.:liferay-workspace-7-3/modules/commerce-discount-rule-by-age/src/main/resources) and delete everything into this folder.

8.  Open the file “CommerceDiscountRuleByAge.java” and change the interface to a class, after that implement “CommerceDiscountRuleType” interface, save it.

9.  Now we’ll add dependencies to our project, open the "build.gradle" file, delete everything into this file and add this code with the dependencies:

 dependencies {
  compileOnly group: "org.osgi", name: "org.osgi.core", version: "5.0.0"
  compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations"
  compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
  compileOnly group: "com.liferay.commerce", name: "com.liferay.commerce.api", version: "22.2.4"
  compileOnly group: "com.liferay.commerce", name: "com.liferay.commerce.discount.api", version: "7.0.0"
  compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
}

10.  Come back to your “CommerceDiscountRuleByAge” class, import the package where the interface is and implement the three methods that are defined by the interface

11.  Create a Key:

public static final String KEY = "key-rule-gold-person";
Remember: “This KEY will identify your module, so it is important to provide a distinct key for each new module (rule type extension), so Liferay Commerce can distinguish the new type from others. If you reuse a existing key that is still in use, you will override the existing module (rule type extension)”

12.  Mark this class as a @Component and inside we setup 3 properties. It should look like this, see below:

@Component(
      immediate = true,
      property = {
              "commerce.discount.rule.type.key=" + CommerceDiscountRuleByAge.KEY,
              "commerce.discount.rule.type.order=:Integer=61"
      },service = CommerceDiscountRuleType.class
)
  • On property array, setup 2 needed parameters for your discount module:

    • "commerce.discount.rule.type.key=" + CommerceDiscountRuleOldPerson.KEY

    •  "commerce.discount.rule.type.order:Integer=61"

The ”commerce.discount.rule.type.order” value indicates how far in the list of available discount rule types this type will appear. At the moment, the last existing on Commerce is with the order value 60, so we’ll use the 61 to be the last one.
  • On service, declare the interface which we are implementing in this class:

    • service = CommerceDiscountRuleType.class . 

13.  Now in the method “getKey” set up “return KEY” that we declared before, and in the method “getLabel” only for the sake of testing a module, set up “return “New Custom Discount Type”. The final @Component class should look like this:

package com.liferay.commerce.discount.rule.by.age;
import com.liferay.commerce.context.CommerceContext;
import com.liferay.commerce.discount.model.CommerceDiscountRule;
import com.liferay.commerce.discount.rule.type.CommerceDiscountRuleType;
import com.liferay.portal.kernel.exception.PortalException;
import org.osgi.service.component.annotations.Component;
import java.util.Locale;


@Component(
      immediate = true,
      property = {
              "commerce.discount.rule.type=" + CommerceDiscountRuleByAge.KEY,
              "commerce.discount.rule.order=:Integer=61"
      },service = CommerceDiscountRuleType.class
)
public class CommerceDiscountRuleByAge implements CommerceDiscountRuleType {

  public static final String KEY = "key-rule-gold-person";

  @Override
  public boolean evaluate(CommerceDiscountRule commerceDiscountRule, CommerceContext commerceContext) throws PortalException {
      return false;
  }

  @Override
  public String getKey() {
      return CommerceDiscountRuleByAge.KEY;
  }

  @Override
  public String getLabel(Locale locale) {
      return "New Custom Discount Type";
  }
}

14.  Deploy it by executing the following command into your “Liferay Workspace” folder.

liferay-workspace-7-3 % ./gradlew deploy

15.  If everything is Ok, the result should look like this (sending your module to your “Liferay Portal/DXP Bundle"):

> Task :modules:commerce-discount-rule-by-age:deploy
Files of project ':modules:commerce-discount-rule-by-age' deployed to /Users/roselainedefaria/Liferay/Source/bundles/osgi/modules

BUILD SUCCESSFUL in 4s


Go to: Open Menu > Commerce > Discounts > Add New Discount. Once the discount is created, at the bottom there is the box for “Rules”, click on “+” to add new.  The new “Discount Rule Type” created should appear on the list in your portal.

                      The code used in this step is available on a tag called “step-1” on github.

Step 2, eliminating the hard-code and internationalizing:

As you know, it is a bad practice using a String  with a hard-coded value like we did in the previous step in the “getLabel”  method (especially on a multi-language portal). It's important to have a multi-language module, like many things in the Liferay world, so we need to transform the module to have multi-language capabilities. 

1.  So... in the “getLabel” method you will use a “ResourceBundle” object. Copy the snippet code below and paste into the “getLabel” method. Then import all  the packages needed.

ResourceBundle resourceBundle = ResourceBundleUtil.getBundle( "content.Language", locale, getClass());
return LanguageUtil.get(resourceBundle, "discount-rule-by-age");

  • The “locale” comes from the current user.

  • The “getClass()” is the classLoader for your resource bundle loader.

  • The “content.Language” is the base language file name. 

2.  Now, you need to create the language properties file in the “resources” folder:

  • Within the resources folder (i.e.: liferay-workspace-7-3/modules/commerce-discount-rule-by-age/src/main/resources), you will create a new folder called “content” 

  • Inside the ”content” folder you have to create the default language file called “Language.properties”,  and in this case the “Language_pt_BR.properties” and “Language_es_ES.properties” files.

  • Copy the key declared before “rule-gold-person” in the “getLabel” method. Include it in the 3 properties files (below you can find  the key with each translation).

In Language.properties:

discount-rule-by-age=Discount Rule By Age                                                                                                                                                         

In Language_pt_BR.properties

discount-rule-by-age=Regra de Desconto por idade

In Language_es_ES.properties

discount-rule-by-age=Regla de Descuento por edad

3.  Save and deploy it by executing the following command inside your Liferay Workspace folder.

liferay-workspace-7-3 % ./gradlew deploy

In Liferay Portal/DXP, go to: Open Menu > Commerce > Discounts > Click on the discount rule created before. At the bottom, there is a box for “Rules”, click on “+” to add a new rule. As the result, on the list, it should appear the new text Discount Rule By Age created.

4.  In order to test other languages (Portuguese and Spanish):

  •  
  • Go to “localhost:8080” and at the end of the URL add “/es-ES/“ (http://localhost:8080/es-ES/), then press enter.
  • Then go to Open Menu > Commerce > Discounts > Click on discount created before, at the bottom, there is the box for “Rules”, click on “+” to add a new one. As the result, on the list, it should appear the new text translated to Spanish Regla de Descuento por edad.

 

  •  
  • Go to “localhost:8080” and at the end of the URL add “/pt-BR/“ (http://localhost:8080/pt-BR/), then press enter.
  • Then go to: Open Menu > Commerce > Discounts > Click on discount created before, at the bottom, there is the box for “Rules”, click on “+” to add a new one. As the result, on the list, it should appear the new text translated to Portuguese “Regra de Desconto por idade.

 

                   Congratulations! So far you have created your first extension Commerce module internationalized.
                                   The code used in this step is available on a tag called “step-2” on github.

Step 3, applying the logic to discount by age:

1.  Go back to your Component class “CommerceDiscountRuleByAge”, inside the evaluatemethod you will retrieve the CommerceOrder from CommerceContext, and validate if commerceOrder is NULL (to stop the flow).

CommerceOrder commerceOrder = commerceContext.getCommerceOrder();

if (commerceOrder == null){
  return false;
}

2.  At the bottom of the class, inject the service “UserLocalService” with the @Reference annotation.

@Reference
protected UserLocalService userLocalService;

3.  In the “evaluate” method, now you will fetch the user, using the “userLocalService”.

User user = userLocalService.fetchUser(commerceOrder.getUserId());

4.  Implement a little more code to get the age from the current user, and validate if the rule will be applied.

Calendar birthdayCal = CalendarFactoryUtil.getCalendar();
birthdayCal.setTime(user.getBirthday());

int birthdayMonth = birthdayCal.get(Calendar.MONTH) +1;
int birthdayDay = birthdayCal.get(Calendar.DATE);
int birthdayYear = birthdayCal.get(Calendar.YEAR);

LocalDate birthdayLocalDate = LocalDate.of(birthdayYear, birthdayMonth, birthdayDay);
LocalDate now = LocalDate.now();
Period period = Period.between(birthdayLocalDate, now);

if(period.getYears() >= 70){
  return true;
}

5.  After finishing, the evaluate method, should look like this:

@Override
public boolean evaluate(CommerceDiscountRule commerceDiscountRule, CommerceContext commerceContext) throws PortalException {

  CommerceOrder commerceOrder = commerceContext.getCommerceOrder();

  if (commerceOrder == null){
      return false;
  }

  User user = userLocalService.fetchUser(commerceOrder.getUserId());

  Calendar birthdayCal = CalendarFactoryUtil.getCalendar();
  birthdayCal.setTime(user.getBirthday());

  int birthdayMonth = birthdayCal.get(Calendar.MONTH) +1;
  int birthdayDay = birthdayCal.get(Calendar.DATE);
  int birthdayYear = birthdayCal.get(Calendar.YEAR);

  LocalDate birthdayLocalDate = LocalDate.of(birthdayYear, birthdayMonth, birthdayDay);
  LocalDate now = LocalDate.now();
  Period period = Period.between(birthdayLocalDate, now);

  if(period.getYears() >= 70){
      return true;
  }

  return false;
}

6.  Save and deploy it by executing the following command inside your “Liferay Workspace”.

liferay-workspace-7-3 % ./gradlew deploy

In addition you can add some logs in this class, see more on my GitHub repo.

The code used in the previous step is available on a tag called “step-3” on github. 

Testing:

  • In Liferay Portal/DXP, go to Open Menu > Commerce > Discounts > Click on the discount created before. At the bottom, there is the box for “Rules”, if there is some rule, delete it, then click on “+” to “Add Discount Rule”. As a result, on the list it should appear the Discount Rule By Age '' created, give a name and press “Submit”. On the list rules, make sure that there is only the discount you created to test it.

  • The discounts settings should be more or less like this (highlighted some important fields to set up):

  • In the Eligibility  tab you can see other configurations to configure the scope this discount applies to “Specific Account Groups”,”Specific Account” or “Specific Channel”.

These are very useful and flexible configurations.  You can play with them later (at this moment, we will leave them with default configuration so they are applied to all accounts and all channels). So for now, don't change anything.

  • Now it is necessary to have a user who is over 70, to test it. Go to: Open Menu > Control Panel > User and Organizations > Click on current user, and change the “Birthday” date in order to make more than 70 years old.

So there you go, module finalized, but we can do better, in the next blog post, you will learn how to make the age configurable and eliminate the "age" hard-coded from your code.

Blogs

I want to thank you for your efforts in posting these articles for commerce. They are quite informative.  Your commitment to help the community is very appreciated. Thanks again.

Thank you Gordon, I'm glad to share it with the community :) 

Please let me know if you find interesting another topic related to Commerce.

Ps.: Just a spoiler I'm writing a new one about payment .

I really liked the payment ones. I posted but for some reason it does not show up. You are really  doing a quality service for the community.