Many base Microsoft events send an email. In this article, learn how to add a new D365 email notification type. Then associate the event with an email template so an email is sent when that event occurs. While a developer could add code to a process to send an email, adding an event leverages helpful existing functionality. Afterwards, an end user can use data to specify whether an email is sent during that event or not. Additionally, they can set up and specify the email template that should be used. Lastly, the existing batch job already set up to send out other emails will send emails set up in this way.
Background
Before learning how to add a D365 email notification type, some background knowledge is helpful.
First, ensure that you have Microsoft Dynamics 365 for Finance and Operations configured to send D365 emails.
Second, understand how to set up a D365 email template.
Third, review the various events in the base system that can send event emails. Ensure you have set up a profile by navigating to Retail and Commerce>Headquarters setup>Commerce email notification profile.
Optionally, but not required, learn how to extend an email template. Or send an email using X++ code.
Now that you understand how email templates and email events work, you can add your own email event.
Code Architecture
Before we just into an example and the code, I find it helpful to understand a few things about the code architecture.
RetailENInfo
First, there is a base class named RetailENInfo. Importantly, the ‘EN‘ stands for ‘Event Notification‘. Therefore, use this class to send any email notification that does not relate to a sales order.
For example, the class RetailENInfo_CustomerCreated handles populating the customer creation placeholders. The ‘resolveENProfileID‘ on the child class populates the ‘CustTable‘ table buffer on the child class for use in initParameterMap. (This will make more sense later).
Therefore, if your scenario sends an email unrelated to a sales order, extend the RetailENInfo class.
Additionally, when an event occurs, call RetailEventNotificationAction::insertRetailENAction.
RetailOENInfo
Second, there is a class named RetailOENInfo that extends RetailENInfo. Importantly, the ‘OEN’, stands for ‘order event notification‘. Therefore, use this class to send any email notification that DOES relate to a sales order.
This class has a class-level ‘SalesTable’ table buffer, which gets populated in the method resolveSalesOrder. Additionally, it already populates many placeholders someone might want to use, saving the developer time. Finally, it also has the method ‘createSalesLineMap‘, which handles the scenario where an email should show repeating sections for each sales line in a sales order.
When an event occurs related to a sales order, call RetailEventNotificationAction::insertRetailOENAction. This method takes in an additional SalesTable parameter.
Example Scenario
Ultimately, the best way to learn is with an example. Currently, there is no existing email event when an order is returned. Pretend, for this example, the business wants to send an email when a return order is invoiced. To do so, perform the following steps. To add any other D365 email notification type, the steps will be similar.
Add A RetailEventNotificationType
First, in a development environment, open Visual Studio, and create a new project.
Second, in the Application Explorer, search for the Base Enum named ‘RetailEventNotificationType‘. Right-click on the enum, and select ‘Create Extension’, if you don’t already have one.

Third, add a new enum value. For this example, I created an enum value named ‘tutorialReturnOrder’.

Fourth, right-click on enum nod, and select Properties. In the properties window, set the Label property on the enum to ‘Return order‘.

Finally, compile the project so that Visual Studio recognizes your new enum value.
Add Notification Action When Event Occurs
When an event occurs, the code needs to insert a record into the RetailEventNotificationAction table. From there, existing logic will determine which email template to use, generate the email, and send it. However, we need to add code to create the record when the event occurs, and pass in the new RetailEventNotificationType.
To better understand, first look at this base code that inserts a record when a packing slip is complete.

Typically, in another scenario, call RetailEventNotificationAction::insertRetailOENAction, pass in the new notification type, along with a RecId, and the sales order record.
However, in this scenario, there is already code that inserts a record when a sales order is being invoiced.

In our scenario, we want the system to determine if the order is a ‘return order’, and if so, create a record with a different notification type.
Notably, there are several ways to potentially do this. We could extend the ‘insert‘ method on CustInvoiceJour table. However, that would result in TWO notification records being inserted.
Instead, create a chain of command class to extend the RetailEventNotificationAction::insertRetailOENAction. Then, add the logic to recognize and create a record with a different notification type than it typically would. Create this class and code:
[ExtensionOf(tableStr(RetailEventNotificationAction))]
final class tutorialRetailEventNotificationAction_Table_Extension
{
public static void insertRetailOENAction(RetailEventNotificationType enType, RefRecId refRecId, SalesTable salesOrder)
{
if (enType == RetailEventNotificationType::OrderInvoiced
&& salesOrder.SalesType == SalesType::ReturnItem)
{
enType = RetailEventNotificationType::tutorialReturnOrder;
}
next insertRetailOENAction(enType,refRecId,salesOrder);
}
}
In addition to extending the insertRetailOENAction method, there is another method we need to modify.
MCRIsRetailNotificationType
Next, there is another method in the RetailEventNotificationAction table that we need to modify. Inside the ‘insertRetailOENAction‘ method, the system performs several checks to determine if a record should be inserted.

Your specific scenario may be different, but in most cases, emails are sent when a sales order is a call center order. In this example, we need to extend the RetailEventNotificationAction::mcrIsRetailNotifictionType method to return true for our new notification type. Add the method mcrIsRetailNotifictionType to the chain of command class created in the last step. The full code should not look like this.
[ExtensionOf(tableStr(RetailEventNotificationAction))]
final class tutorialRetailEventNotificationAction_Table_Extension
{
public static void insertRetailOENAction(RetailEventNotificationType enType, RefRecId refRecId, SalesTable salesOrder)
{
if (enType == RetailEventNotificationType::OrderInvoiced
&& salesOrder.SalesType == SalesType::ReturnItem)
{
enType = RetailEventNotificationType::tutorialReturnOrder;
}
next insertRetailOENAction(enType,refRecId,salesOrder);
}
public static boolean mcrIsRetailNotificationType(RetailEventNotificationType _enType,
SalesTable _salesTable)
{
boolean ret = next mcrIsRetailNotificationType(_enType, _salesTable);
switch (_enType)
{
case RetailEventNotificationType::tutorialReturnOrder:
ret = true;
break;
default:
break;
}
return ret;
}
}
Extend The RetailOENInfo class
After an event record is inserted, the system loops through all unprocessed records to create the email and send it. When you make a new D365 email notification type, it may work on a new record, other than a sales order, or process different records and placeholders on a sales order. Therefore, we need a new child class of the RetailOENInfo class to handle this.
The Code
Therefore, create a new class that extends the RetailOENInfo class. See this code that implements our example.
class tutorialRetailOENInfo_ReturnOrder extends RetailOENInfo
{
private CustInvoiceJour invoiceJournal;
#define.CustAccount('CustAccount')
#define.Deadline('Deadline')
#define.RMANumber('RMANumber')
#define.ToReturn('ToReturn')
#define.Returned('Returned')
public RetailEventNotificationType EventNotificationType()
{
return RetailEventNotificationType::tutorialReturnOrder;
}
public void resolveSalesOrder(RefRecId _refRecId)
{
this.invoiceJournal = CustInvoiceJour::findRecId(_refRecId);
this.parmSalesOrder(SalesTable::find(invoiceJournal.SalesId));
}
protected str generateSaleslineTable(str _saleslineTableTemplate)
{
SalesLine salesLine;
CustInvoiceTrans custInvoiceTrans;
str result;
Map saleslineMap;
MCRSalesLine mcrSalesLine;
RetailGiftCardTransactions retailGiftCardTransactions;
while select LineNum from custInvoiceTrans
where custInvoiceTrans.SalesId == invoiceJournal.SalesId
&& custInvoiceTrans.InvoiceDate == invoiceJournal.InvoiceDate
&& custInvoiceTrans.InvoiceId == invoiceJournal.InvoiceId
&& custInvoiceTrans.numberSequenceGroup == invoiceJournal.numberSequenceGroup
join salesLine
where salesLine.SalesId == salesOrder.SalesId
&& salesLine.InventTransId == custInvoiceTrans.InventTransId
outer join firstOnly GiftCardGiftMessage from mcrSalesLine
where mcrSalesLine.SalesLine == salesLine.RecId
outer join firstOnly Amount, CardNumber from retailGiftCardTransactions
where retailGiftCardTransactions.MCRInventTransId == salesLine.InventTransId
&& retailGiftCardTransactions.Operation == RetailGiftCardOperation::Issue
{
saleslineMap = this.createSalesLineMap(salesLine, retailGiftCardTransactions, mcrSalesLine);
result += SysEmailMessage::stringExpand(_saleslineTableTemplate, saleslineMap);
}
return result;
}
protected SalesQty getSalesLineQtyShipped(SalesLine _salesLine)
{
CustInvoiceTrans custInvoiceTrans;
select firstonly custInvoiceTrans
where custInvoiceTrans.SalesId == _salesLine.SalesId
&& custInvoiceTrans.InvoiceDate == invoiceJournal.InvoiceDate
&& custInvoiceTrans.InvoiceId == invoiceJournal.InvoiceId
&& custInvoiceTrans.InventTransId == _salesLine.InventTransId
&& custInvoiceTrans.numberSequenceGroup == invoiceJournal.numberSequenceGroup;
return custInvoiceTrans.Qty;
}
protected void initParameterMap()
{
super();
parameterMap.insert(#CustAccount, salesOrder.CustAccount);
parameterMap.insert(#Deadline, date2Str(salesOrder.ReturnDeadline, 321, DateDay::Digits2, DateSeparator::Slash, DateMonth::Digits2, DateSeparator::Slash, DateYear::Digits4));
parameterMap.insert(#RMANumber, salesOrder.ReturnItemNum);
}
protected Map createSalesLineMap(SalesLine _salesLine,
RetailGiftCardTransactions _retailGiftCardTransactions,
MCRSalesLine _mcrSalesLine)
{
Map salesLineMap = super(_salesLine, _retailGiftCardTransactions, _mcrSalesLine);
SalesQty toReturn, returned;
SalesLine salesLineSum;
select sum(ExpectedRetQty), sum(SalesQty)
from salesLineSum
where salesLineSum.ItemId == _salesLine.ItemId
&& salesLineSum.SalesId == _salesLine.SalesId;
toReturn = salesLineSum.ExpectedRetQty;
returned = salesLineSum.SalesQty;
saleslineMap.insert(#ToReturn, RetailENInfo::formatQtyData((toReturn - returned), languageId, _salesLine.SalesUnit));
saleslineMap.insert(#Returned, RetailENInfo::formatQtyData(returned, languageId, _salesLine.SalesUnit));
return salesLineMap;
}
}
Explaining The Code
To explain, there are several methods that need to be implemented in this child class.
First, like we did to extend an email template, add any placeholders needed for this event’s email template.

Second, add the method ‘EventNotificationType‘, and return the new enum value you added.

Third, add the method ‘resolveSalesOrder‘. This takes the RecId of the record passed into the second parameter of the insertRetailOENAction method, and uses it to find and set the sales order in the base class.

Fourth, add the method ‘generateSaleslineTable’. To explain, the system takes the HTML code from the email template that is inside the tags ‘<!–%tablebegin.salesline% –>’ and ‘<!–%tableend.salesline%–>’. Next, it passes that text into this method. Then, this method loops through each relevant sales order line, creating a filled-in copy of the HTML code for each sales line. Ultimately, it returns one longer string with all of the sales line HTML text.

Fifth, add the initiParameterMap method to fill in any new placeholders added to the email template.

Sixth, add the ‘createSalesLineMap method to fill any any new placeholders added in the sales line section of the email template.

Optionally Add InitSetting
Seventh, optionally, add the ‘initSetting‘ method if the system needs to use a different email notification profile than the one specified in the commerce parameters. For example, the profile ID can be specified on each channel. If you want the system to use that profile, you need to set the notificationSetting and notificationProfile class-level table buffers. See this example from the MCRRetailOENInfo_GiftCardIssue class.

To explain further, if you do override the base class’s initSetting method, you must make sure you do three things to ensure your version does everything that the base class method does:
- You must set the notificationSetting and notificationProfile class-level table buffers. This tells the system which profile to use, and therefore which email template to use.
- You must call the method resolveSalesOrder when working with a sales order. The base class’s initSetting method calls resolveENProfileID, which calls resolveSalesOrder inside of it. If you are not working with a sales order, you should override the resolveENProfileID on the child class and set the class-level table buffer that contains the information for your email. For example, set the CustTable table. See the class RetailENInfo_CustomerCreated.
- You must call the method initParameterMap. This populates all of the header level placeholder values.
Add To The Construct Method
When the batch job runs to process the inserted records, the system needs to know which child class to use when building the email template. Create a chain of command class that extends the constructUninitialized method on the RetailENInfo class. To explain, the class RetailOENInfo extends RetailENInfo. So extending the base class’s construct method works whether your email is based on a sales order or not.
[ExtensionOf(classStr(RetailENInfo))]
final class tutorialRetailENInfo_Class_Extension
{
protected static RetailENInfo constructUninitialized(RetailEventNotificationType _notificationType)
{
RetailENInfo notificationInformation = next constructUninitialized(_notificationType);
switch (_notificationType)
{
case RetailEventNotificationType::tutorialReturnOrder:
notificationInformation = new tutorialRetailOENInfo_ReturnOrder();
break;
default:
break;
}
return notificationInformation;
}
}
It is a bit confusing, but you do not extend the ‘construct‘ method because it calls ‘constructUninitialized‘ inside of it. Additionally, it calls ‘initSetting‘, which we want it to do. If we were to extend the ‘construct’ method, we would also need to call initSetting. Doing it this way requires a little less code.

Setup The New Event
Before testing, set up the new D365 email notification type.
First, navigate to Retail and Commerce>Headquarters setup>Commerce email notification profile. In your profile, add a new record to the ‘Retail event notification settings’ grid.
Second, set the ‘Email notification type‘ to your new type. In this example, set it to ‘Return order‘.
Third, set the ‘Email ID’ to the email template you have set up. If you have not set one up, navigate to Organization administration>Setup>Organization email templates. Mark the event notification as ‘Active‘.

Testing
To test the new D365 email notification type, perform the action that triggers the event. For this example, create and invoice an RMA return order.
Next, run the batch job Retail and Commerce>Retail and Commerce IT>Email and notifications>Send email notification, or wait for it to run on recurrence.
Then, check your email. Confirm the email was sent, and that it used any additional placeholders you included in your child class.

Microsoft Documentation
I was not able to find any documentation specifically about extending the RetailENInfo class. However, this Microsoft Documentation lists out many of the existing notification types. This will be helpful in determining if you need to add a new event notification.
Conclusion
Fortunately, many changes in a sales order’s life cycle can already trigger emails. However, businesses may have additional functionality where email communication with a customer would be helpful. This article explains the steps to add a new D365 email notification type while leveraging existing forms and batch jobs. This allows for efficient development time and easier user adoption.
Leave a Reply