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.
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.
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.q2.11
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:
-
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
-
Build and deploy the example.
./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
NoteThis command is the same as copying the deployed jars to
/opt/liferay/osgi/modules
on the Docker container. -
Confirm the deployment in the Docker container console.
STARTED com.acme.g2f3.impl_1.0.0
-
Log in as an administrator, open the Global Menu (), click on Control Panel → Sites, and add a new Minium Demo site.
-
After creating the site, open the Global Menu () again and go to Control Panel → Language Override. Click Add () 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 ImportantYou 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. -
Now, open the Global Menu () and go to Commerce → Channels.
-
Select Minium and go to Notification Templates.
-
Click Add () 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
-
Click Save.
-
Log out and log in as a buyer and place a new order in the store.
-
Log out and log back in as an administrator, open the Global Menu (), and go to Commerce → Orders.
-
Select the order and click Accept Order. Then click Create Shipment.
-
Check your inbox for the received notification.
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
- Review the CommerceNotificationType interface
- Complete the Notification Type
- Create a ModelListener for CommerceShipment
- Review the CommerceDefinitionTermContributor interface
- Complete the Term Contributors
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.