oo

Implementing a Custom Notification Type

You can configure Liferay Commerce to send email notifications for a variety of event triggers in your store. When there’s no out-of-the-box notification trigger that fits your needs, you can implement your own.

To add a new notification type, you must implement the CommerceNotificationType interface. See Sending Emails to learn how to set up a Notification Template and view the OOTB types available.

Note

This tutorial uses the Minium Demo site initializer.

Overview of a Notification Type

Notifications are scoped to a Channel. You can create a new Notification Template under Channel settings, and it gets triggered based on the notification type.

A notification template defines a flow of events.

The diagram above shows a Notification Template of type G2F3 Shipment Created. This gets triggered for the creation of new shipments. During the creation of a shipment, a notification is sent to the recipient/recipients as mentioned in the Notification Template. You can use wildcards in the To, Subject, and Body fields of the template, and these get resolved before sending.

Deploying the Notification Type and Adding Language Keys

Start a new Liferay DXP instance by running

docker run -it -m 8g -p 8080:8080 liferay/dxp:2024.q1.1

Sign in to Liferay at http://localhost:8080 using 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 Notification Type.

    curl https://resources.learn.liferay.com/commerce/latest/en/developer-guide/order-management/liferay-g2f3.zip -O
    
    unzip liferay-g2f3.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.g2f3.impl_1.0.0
    
  4. Log in as an administrator, open the Global Menu (Applications Menu icon), click on Control PanelSites, and add a new Minium Demo site.

  5. After creating the site, open the Global Menu (Applications Menu icon) again and go to Control PanelLanguage Override. Click Add (Add icon) and add the following keys.

    Language Key Value
    g2f3-shipment-created G2F3 Shipment Created
    g2f3-shipment-creator-name-definition-term Name of the account that created the order
    g2f3-order-shipping-address-definition-term Shipping Address
    g2f3-shipment-id-definition-term Shipment ID
    g2f3-shipment-creator-email-definition-term Email of the user that created the shipment
    Important

    You can add language keys in the Language Override tool for Liferay DXP 7.4 U4+ or Liferay Portal 7.4 GA8+. For previous versions, you must add a Language.properties file under /src/main/resources/content/ with the keys before building and deploying.

  6. Now, open the Global Menu (Applications Menu icon) and go to CommerceChannels.

  7. Select Minium and go to Notification Templates.

  8. Click Add (Add icon) to create a new template:

    Name: Testing G2F3 Shipment Created

    Type: G2F3 Shipment Created

    To: [%SHIPMENT_CREATOR_EMAIL%]

    From Address: test@liferay.com

    From Name: Administrator

    Subject: New Shipment Created - Shipment ID: [%SHIPMENT_ID%]

    Body:

    Hi,

    A new shipment has been created by [%SHIPMENT_CREATOR_NAME%]

    Shipping Address: [%ORDER_SHIPPING_ADDRESS%]

    Thanks,

    Admin

  9. Click Save.

  10. Log out and log in as a buyer and place a new order in the store.

  11. Log out and log back in as an administrator, open the Global Menu (Applications Menu icon), and go to CommerceOrders.

  12. Select the order and click Accept Order. Then click Create Shipment.

  13. Check your inbox for the received notification.

Important

You can use a fake SMTP server like MockMock(https://github.com/tweakers/MockMock) to test these notifications in your local development environment. Add the following line in your portal-ext.properties file: mail.send.blacklist=noreply@liferay.com, noreply@domain.invalid, test@domain.invalid. Run the jar using java -jar MockMock.jar and check localhost:8282 for the received emails.

How the Custom Notification Type Works

This example consists of six main steps. First, you must annotate the class for OSGi registration. Next, review the CommerceNotificationType interface. Then, finish the implementation of the custom CommerceNotificationType.

After that, create a ModelListener for the CommerceShipment class. Next, review the CommerceDefinitionTermContributor interface. Finally, implement term contributors to resolve the wildcards for the new notification.

Annotate the class for OSGi Registration

You must provide a distinct key for the notification type so that Liferay Commerce can distinguish it from others in the notification status registry. Specifying a key that is already in use overrides the existing associated type. The order determines its sort order in the drop down. In this case, the Order Awaiting Shipment notification type has the order as 50, and the Order Partially Shipped notification type has the order as 60. To place the status between the two, the order must be between those two numbers, in this case, 51.

Review the CommerceNotificationType interface

Implement the following methods:

public String getClassName(Object object);

This method returns the name of the class for which the notification type is implemented.

public long getClassPK(Object object);

This method returns the primary key of the object.

public String getKey();

This method returns the unique key of the notification type. Using an existing key overrides that notification type.

public String getLabel(Locale locale);

This method returns the name of the notification type as it appears in the UI. This name may be a language key or a string.

Complete the Notification Type

@Override
public String getClassName(Object object) {
	if (!(object instanceof CommerceShipment)) {
		return null;
	}

	return CommerceShipment.class.getName();
}

@Override
public long getClassPK(Object object) {
	if (!(object instanceof CommerceShipment)) {
		return 0;
	}

	CommerceShipment commerceShipment = (CommerceShipment)object;

	return commerceShipment.getPrimaryKey();
}

@Override
public String getKey() {
	return "g2f3";
}

@Override
public String getLabel(Locale locale) {
	return LanguageUtil.get(locale, "g2f3-shipment-created");
}

To complete the Notification type implementation, you must implement the above methods. In the first method, you check if the object is of type CommerceShipment and return its class name if it’s true. In the second method, you check this again and return the shipment’s primary key if it’s true. The third method returns the unique key and the last method returns the label that appears on the UI.

Create a ModelListener for CommerceShipment

@Component(service = ModelListener.class)
public class G2F3CommerceShipmentModelListener
	extends BaseModelListener<CommerceShipment> {

	@Override
	public void onAfterCreate(CommerceShipment commerceShipment)
		throws ModelListenerException {

		try {
			_commerceNotificationHelper.sendNotifications(
				commerceShipment.getGroupId(), commerceShipment.getUserId(),
				"g2f3", commerceShipment);
		}
		catch (PortalException portalException) {
			if (_log.isDebugEnabled()) {
				_log.debug(portalException);
			}
		}
	}

	private static final Log _log = LogFactoryUtil.getLog(
		G2F3CommerceShipmentModelListener.class);

	@Reference
	private CommerceNotificationHelper _commerceNotificationHelper;

}

For triggering the notification every time a shipment is created, you must extend the BaseModelListener class that implements the ModelListener interface. This interface has methods for the entity that are triggered for events like create, update, delete, etc. You can use the onAfterCreate(T model) method to trigger the notification upon creation of a shipment.

Review the CommerceDefinitionTermContributor interface

Implement the following methods:

public String getFilledTerm(String term, Object object, Locale locale) throws PortalException;

This method replaces the wildcard with its appropriate value and returns it as a String.

public String getLabel(String term, Locale locale);

This method returns the name of the term contributor as it appears in the UI. This name may be a language key or a string.

public List<String> getTerms();

This method returns all the term contributors available for the Notification type.

Complete the Term Contributors

Term contributors resolve the wildcards present in the To, Subject, and Body fields. There are two term contributors implemented in this example: one for the Subject and Body fields and the other for the To field.

Implement the getFilledTerm method for the Body and Subject

@Override
public String getFilledTerm(String term, Object object, Locale locale)
	throws PortalException {

	if (!(object instanceof CommerceShipment)) {
		return term;
	}

	CommerceShipment commerceShipment = (CommerceShipment)object;

	if (term.equals(_SHIPMENT_CREATOR_NAME)) {
		AccountEntry accountEntry = commerceShipment.getAccountEntry();

		if (accountEntry.isPersonalAccount()) {
			User user = _userLocalService.getUser(accountEntry.getUserId());

			return user.getFullName(true, true);
		}

		return accountEntry.getName();
	}

	if (term.equals(_ORDER_SHIPPING_ADDRESS)) {
		CommerceAddress commerceAddress =
			commerceShipment.fetchCommerceAddress();

		return commerceAddress.getStreet1() + ", " +
			commerceAddress.getCity() + ", " + commerceAddress.getZip();
	}

	if (term.equals(_SHIPMENT_ID)) {
		if (commerceShipment == null) {
			return term;
		}

		return String.valueOf(commerceShipment.getCommerceShipmentId());
	}

	return term;
}

Before resolving the wildcard, there are checks to verify whether the object is null or of type CommerceShipment. Then, if the term contains the wildcard, the wildcard gets replaced with the shipment creator’s name, shipping address, or the shipment ID. For the shipment creator’s name, the name of the account from the shipment is returned. The shipping address is returned as a concatenated string of the street address, city, and zip. The shipment ID is returned from the shipment object directly.

Implement the getFilledTerm method for the Recipient

@Override
public String getFilledTerm(String term, Object object, Locale locale)
	throws PortalException {

	if (!(object instanceof CommerceShipment)) {
		return term;
	}

	CommerceShipment commerceShipment = (CommerceShipment)object;

	if (commerceShipment == null) {
		return term;
	}

	if (term.equals(_SHIPMENT_CREATOR_EMAIL)) {
		AccountEntry accountEntry = commerceShipment.getAccountEntry();

		if (accountEntry.isPersonalAccount()) {
			User user = _userLocalService.getUser(accountEntry.getUserId());

			return String.valueOf(user.getUserId());
		}

		return String.valueOf(commerceShipment.getUserId());
	}

	return term;
}

Before resolving the wildcard, there are checks to verify whether the object is null or of type CommerceShipment. Then, if the term contains the wildcard, the wildcard gets replaced with the user ID of the account. When a notification is sent, it uses this ID to find the user’s email.

Implement the getLabel and getTerms methods

The getLabel method returns the name of the terms as it appears in the UI. You can use language keys to do this or directly return a string.

@Override
public String getLabel(String term, Locale locale) {
	return LanguageUtil.get(locale, _languageKeys.get(term));
}

This method returns all the term contributors available for the Notification type. You can use language keys to do this or directly return a hard coded string that displays the term in the UI.

@Override
public List<String> getTerms() {
	return new ArrayList<>(_languageKeys.keySet());
}

Conclusion

Congratulations! You now know the basics for implementing the CommerceNotificationType interface. You also know the basics of how notifications work and the use of a MessageListener to send your own notification type.

Capability: