oo

Implementing a Custom Order Validator

This tutorial explains 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 provides multiple out-of-the-box order validators, including a default, as well as validators to check item versions and recurring items (subscriptions).

The order validator has validation logic for both adding a product to the cart and proceeding to a new checkout step. There are three parts:

  1. Validation logic for adding a product to cart.
  2. Validation logic for proceeding to checkout.
  3. Language keys added to Language.properties.

The two validate methods are where you define the custom validation logic for the order validator. This example adds logic to reject orders with more than ten of an item over a certain price.

Deploy the Sample Order Validator

Start a new Liferay instance by running

docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.112-ga112

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://resources.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 the addition of the example order validator 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.

    The custom order validator displays an error message.

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

Creating an Order Validator consists of three main steps. First, you annotate the class for OSGi registration. Second, you implement the CommerceOrderValidator interface. Finally, you create your 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
)

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

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

Review the CommerceOrderValidator Interface

The interface requires that you implement three methods:

public String getKey();

This method provides a unique identifier for the order validator in the order validator registry. The key fetches the validator from the registry. Reusing a key that is already in use overrides 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 you add your 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 for more information.

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

This is the second validation method where you can add custom validation logic. This method is called for items already in the cart, whenever the order transitions to In Progress or Pending.

Validation Logic for Adding a Product to Cart

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

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

	BigDecimal price = cpInstance.getPrice();

	if ((price.doubleValue() > _MAX_ITEM_PRICE) &&
		BigDecimalUtil.gt(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",
				_MAX_ITEM_QUANTITY.toString()));
	}

	return new CommerceOrderValidatorResult(true);
}
private static final double _MAX_ITEM_PRICE = 100.0;

private static final int _MAX_ITEM_QUANTITY = 10;

The main validation in the example checks if both the price (stored as a BigDecimal) is more than $100, and the quantity is greater than ten. This price information is available from the CPInstance, which contains information about the customer order. To find more methods you can use with a CPInstance, see CPInstance and CPInstanceModel.

Note

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

Validation Logic for Proceeding to Checkout

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

	BigDecimal price = commerceOrderItem.getUnitPrice();

	if ((price.doubleValue() > _MAX_ITEM_PRICE) &&
		BigDecimalUtil.gt(
			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",
				_MAX_ITEM_QUANTITY.toString()));
	}

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

Language Keys added to Language.properties

Add the language keys and their values to a Language.properties file within your module. For example,

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.

Modifying the Custom Order Validator

To change what your order validator does, edit your java file. Make the validator reject orders worth over $200 by changing the value of _MAX_ITEM_PRICE. Redeploy your custom order validator to send these changes to Liferay.

In your browser, try adding 10 items worth between $100 and $200. You can add these items to your cart because your validator no longer rejects orders over $100.

Try adding 10 items worth over $200. If you cannot add these items to your cart, your validator is working!

Conclusion

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

Capability: