Documentation

Implementing a Custom Order Validator

This tutorial will show you how to add a custom order validator by implementing the CommerceOrderValidator interface.

An order validator is a class that validates items in a customer’s cart when proceeding through checkout. Liferay Commerce provides multiple out-of-the-box order validators, including a default, as well as validators to check item versions and recurring items (subscriptions).

Deploy an Example

In this section, we will get an example order validator up and running on your instance of Liferay Commerce.

Start a new Liferay instance by running

docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.29-ga29

Sign in to Liferay at http://localhost:8080. Use the email address test@liferay.com and the password test. When prompted, change the password to learn.

Then, follow these steps:

  1. Download and unzip the Acme Commerce Order Validator.

    curl https://learn.liferay.com/commerce/latest/en/developer-guide/sales/liferay-n9b2.zip -O
    
    unzip liferay-n9b2.zip
    
  2. Build and deploy the example.

    ./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
    

    Note

    This command is the same as copying the deployed jars to /opt/liferay/osgi/modules on the Docker container.

  3. Confirm the deployment in the Docker container console.

    STARTED com.acme.n9b2.impl_1.0.0
    
  4. Verify that the example order validator was added by viewing the failure message. Open your browser to https://localhost:8080 and navigate to a catalog with at least one item priced over $100. If no such product exists yet, add it yourself; see Creating a Simple Product for more information.

    From the catalog, find the item with this price, then click “Add to Cart”. Increase the quantity to 11 or more, then click the arrow to continue. The error message that appears shows that the custom order validator successfully rejected adding the item.

    New order validation error message

Congratulations, you’ve successfully built and deployed a new order validator that implements CommerceOrderValidator.

Next, let’s dive deeper to learn more.

Walk Through the Example

In this section, we will review the example we deployed. First, we will annotate the class for OSGi registration. Second, we will review the CommerceOrderValidator interface. And third, we will complete our implementation of CommerceOrderValidator.

Annotate the Class for OSGi Registration

@Component(
    property = {
    "commerce.order.validator.key=n9b2",
    "commerce.order.validator.priority:Integer=9"
    },
    service = CommerceOrderValidator.class
)
public class N9B2CommerceOrderValidator implements CommerceOrderValidator {

It is important to provide a distinct key for the order validator so that Liferay Commerce can distinguish the new order validator from others in the order validator registry. Reusing a key that is already in use will override the existing associated validator.

The commerce.order.validator.priority value indicates when the order validator will perform its validation in sequence with other validators. For example, the default order validator has a value of 10. Giving our order validator a value of 9 ensures that it will perform its validation immediately before the default validator.

Review the CommerceOrderValidator Interface

Implement the following methods:

public String getKey();

This method provides a unique identifier for the order validator in the order validator registry. The key can be used to fetch the validator from the registry. Reusing a key that is already in use will override the existing associated validator.

public CommerceOrderValidatorResult validate(Locale locale, CommerceOrder commerceOrder, CPInstance cpInstance, int quantity) throws PortalException;

This is one of the two validation methods where we will add our custom validation logic. This method is called whenever a customer adds an item to their cart. It does this by returning a CommerceOrderValidatorResult, which uses a boolean to signal whether or not the result passes validation; see CommerceOrderValidatorResult.java.

public CommerceOrderValidatorResult validate(Locale locale, CommerceOrderItem commerceOrderItem) throws PortalException;

This is the second validation method where we can add custom validation logic. This method is called for items already in the cart, whenever a customer proceeds to a new step in checkout.

Complete the Order Validator

The order validator is comprised of validation logic for both adding a product to the cart and proceeding to a new checkout step. Do the following:

The two validate methods are where we define the custom validation logic for our order validator. In our example, we will add logic to reject orders with more than ten of an item over a certain price.

Add Validation Logic for Adding a Product to Cart

@Override
public CommerceOrderValidatorResult validate(
        Locale locale, CommerceOrder commerceOrder, CPInstance cpInstance,
        int quantity)
    throws PortalException {

    if (cpInstance == null) {
        return new CommerceOrderValidatorResult(false);
    }

    BigDecimal price = cpInstance.getPrice();

    if ((price.doubleValue() > _MAX_ITEM_PRICE) &&
        (quantity > _MAX_ITEM_QUANTITY)) {

        ResourceBundle resourceBundle = ResourceBundleUtil.getBundle(
            "content.Language", locale, getClass());

        return new CommerceOrderValidatorResult(
            false,
            LanguageUtil.format(
                resourceBundle,
                "this-expensive-item-has-a-maximum-quantity-of-x",
                Integer.valueOf(_MAX_ITEM_QUANTITY)));
    }

    return new CommerceOrderValidatorResult(true);
}

private static final double _MAX_ITEM_PRICE = 100.0;

private static final int _MAX_ITEM_QUANTITY = 10;

The main validation check for our example is to check if both the price (stored as a BigDecimal) is more than $100, and the quantity is greater than ten. We get the price information from the CPInstance, which contains information about the order the customer has added. To find more methods you can use with a CPInstance, see CPInstance and CPInstanceModel.

Note that it is best practice to include a localized message explaining why the validation failed for the main validation checks.

Add Validation Logic for Proceeding in Checkout

@Override
public CommerceOrderValidatorResult validate(
        Locale locale, CommerceOrderItem commerceOrderItem)
    throws PortalException {

    BigDecimal price = commerceOrderItem.getUnitPrice();

    if ((price.doubleValue() > _MAX_ITEM_PRICE) &&
        (commerceOrderItem.getQuantity() > _MAX_ITEM_QUANTITY)) {

        ResourceBundle resourceBundle = ResourceBundleUtil.getBundle(
            "content.Language", locale, getClass());

        return new CommerceOrderValidatorResult(
            false,
            LanguageUtil.format(
                resourceBundle,
                "expensive-items-have-a-maximum-order-quantity-of-x",
                Integer.valueOf(_MAX_ITEM_QUANTITY)));
    }

    return new CommerceOrderValidatorResult(true);
}

Add the same validation logic to this method, since it will be called for the items in the customer’s cart. The main difference in this case is we get the information from a CommerceOrderItem object; see CommerceOrderItem and CommerceOrderItemModel to find more methods you can use with a CommerceOrderItem.

Add the Language Keys to Language.properties

Add the language keys and their values to a Language.properties file within our module:

expensive-items-have-a-maximum-order-quantity-of-x=Expensive items have a maximum order quantity of {0}.
this-expensive-item-has-a-maximum-quantity-of-x=This expensive item has a maximum order quantity of {0}.

See Localizing Your Application for more information.

Conclusion

Congratulations! You now know the basics for implementing the CommerceOrderValidator interface, and have added a new order validator to Liferay Commerce.