In my main blog post regarding Dynamics AX Retail/3rd party integration I mentioned leveraging Real Time Service to allow middleware (like Microsoft BizTalk) and 3rd party systems to report to AX on the results of their actions. As the meta data is written into the standard Dynamics AX Retail download sessions and upload sessions forms, these forms will now serve system administrators to have visibility and control even beyond their the Dynamics AX Retail CDX landscape (CDX -> see rectangle in the middle, beyond CDX -> bottom rectangle):

Microsoft Dynamics AX Retail - CDX meta data
In this blog I’ll provide the X++ source code to enable this and I’ll mention some tiny features I added to the download sessions and upload sessions forms to maximize control on our 3rd party integration. First I’ll introduce you to the topic by explaining a bit more on how to extend Real Time Service for access from 3rd party systems.

How to extend Real Time Service

What I like most about Real Time Service is that it provides a single-point-of-access to any method in AX from the outside world. With ‘single-point-of-access’ I mean that with Real Time Service we don’t need to expose an endless number of services if for example we want to get customer information, create a customer and cancel an order – we can stick to 1 service only as the AX method we want to call into through the service is a parameter on the service. So if we want to create a customer in AX we tell the service to invoke the RetailTransactionService::newCustomer method and we provide the service with the input parameters the respective method in AX expects:

Microsoft Dynamics AX Retail - RTS XML
Message for calling the Real Time Service (newCustomer method) = The “sender”

Microsoft Dynamics AX Retail - RTS customizationAX HQ newCustomer method = The “receiver”

So basically you can call into any method in AX from 3rd party systems just by passing the right parameters and sending the information in the right message format. In a later blog I’ll provide more details in this particular area – for now I stick to mentioning some basic ‘rules of play’:

  1. Real Time Service is designed to call into the RetailTransactionService and RetailTransactionServiceEx classes in AX by default. Although even this is configurable in the web.config file of the RealTimeService application on IIS, I’d stick to this standard. In practice this will never block you from leveraging any method in AX. Just follow the path Microsoft is on since the AX 2012 R3 release, to leverage the methods in the RetailTransactionService and RetailTransactionServiceEx classes only as ‘gateway’ to other classes not including any business logic in itself. It basically comes down to the following structure (see left hand side):Microsoft Dynamics AX Retail - RTS architecture
  2. Before going into customization, carefully check the richness of the current RetailTransactionService class first. Microsoft has exposed more than 100 Retail related methods packed with business logic through this class. So the most common Retail operations (which are normally exposed to the native AX channels ;-)) are available. In general, it’s easy to expose existing business logic by just creating a custom ‘gateway’ method to call into existing business logic. So rule of thumb should be that minimal coding is required, otherwise you might have chosen the wrong path.
  3. Put all custom (‘gateway’) methods in the RetailTransactionServiceEx classes. Invoking these methods is nothing different than invoking the methods in the RetailTransactionService class except for replacing the node InvokeMethod by InvokeExtensionMethod in the example XML above.

Custom RetailTransactionServiceEx:: newRetailCDXMetaDataRecord

With this custom method (see source code below), both Retail > Inquiries > Commerce data exchange > Upload sessions and Download sessions table/forms are served. This is directed by the downloadMessage boolean field which is 1 (true) if the respective interface flow is a download flow (AX -> native AX/3rd party) and 0 (false) if the respective interface flow is an upload flow (naïve AX/3rd party). The artifacts prefixed with ‘pmo’ are discussed in the next section of this blog. First I’ll list you the input/output variables for the method – the array element type indicates what the type of parameter is that your XML message to Real Time Service requires for the variable. Note for the X++ code that I included many validations to ensure errors are not raised due to XML variable type to AX variable type conversion issues.

5. Table1

6. Table2
/// <summary>
/// Method is added by PMOU (16.06.2015)
/// Method is used for creating a RetailCDXDataStoreSession record which reflects CDX meta data
/// BizTalk leverages the method to report on the result of its part of the AX/3rd party channel interface chain
/// In doing so, system administrators have complete visibility and control across the complete interface chain including integration to 3rd party channels
/// </summary>
/// <returns>
/// Container with entity keys
/// </returns>

public
static
container newRetailCDXMetaDataRecord(boolean downloadMessage,
RetailCDXSharePath         sDataFileOutputPath,
RetailCDXDataGroupRefRecId     sDatagroupRecId,
RetailConnJobId         sJobId,
RetailCDXRowsAffected         sRowsAffected,
RetailCDXScheduleRefRecId     sScheduleRecId,
RetailCDXSessionNumber     sSessionNumber,
RetailCDXDownloadSessionStatus     sDownloadStatus,
pmoRetailCDXLinkedSessionRefRecId     sdsLinkedSessionRecId,
RetailCDXDataStoreRefRecId     sdsDataStoreRecId,
str         sdsDateApplied,
str         sdsDateDownloaded,
str         sdsDateRequested,
RetailCDXMonDataSyncMessage     sdsMessage,
RetailCDXDownloadSessionStatus     sdsDownloadStatus,
RetailCDXCheckSum     uCheckSum,
RetailCDXDataStoreRefRecId     uDataStoreRecId,
str         uDateCreated,
str         uDateUploaded,
RetailCDXFileSize         uFileSize,
RetailCDXHqUploadSessionID     uHQUploadSessionId,
RetailConnJobId         uJobId,
RetailCDXMonDataSyncMessage     uMessage,
RetailCDXUploadSessionRerun     uRerun,
RetailCDXSessionNumber     uRerunForSessionId,
RetailCDXRowsAffected     uRowsAffected,
RetailCDXScheduleRefRecId     uScheduleRecId,
RetailCDXUploadSessionStatus     uUploadStatus,
RetailCDXTryCount         uTryCount,
RetailCDXPacketFilePath     uUploadPath,
RetailCDXSessionNumber     uUploadSessionId,
pmoRetailCDXLinkedSessionRefRecId     uLinkedSessionRecId,
pmoRetailCDXDataSource     uDataSource)
{
boolean         success = false;
str             error = ;
Counter         infologline = infolog.num();
int             fromLine;
str             xmlResult = ;
container         tmpResult;
RetailCDXDownloadSessionRefRecId     downloadSessionRecId;
RetailCDXDownloadSession     retailCDXDownloadSession;
retailCDXDownloadSessionDataStore     retailCDXDownloadSessionDataStore;
RetailCDXUploadSession     retailCDXUploadSession;
RetailCDXUploadSessionLog     retailCDXUploadSessionLog;
RefRecId         sessionRecId;
RetailCDXDateApplied         sdsdateAppliedUTC; //UTCDateTime
RetailCDXDateDownloaded     sdsdateDownloadedUTC; //UTCDateTime
RetailCDXDateRequested     sdsdateRequestedUTC; //UTCDateTime
RetailCDXDateCreated         udateCreatedUTC; //UTCDateTime
RetailCDXDateUploaded     udateUploadedUTC; //UTCDateTime
try
{
fromLine = Global::infologLine();
// Validate if this is an AX inbound or outbound flow

if (downloadMessage == true)
{
// If SessionNumber is 0 then create a new session

if (ssessionNumber == 0)
{
ssessionNumber = RetailCDXSessionIDGenerator::getNextSession();
}
if (ssessionNumber > 0 && sjobId && sdatagroupRecId && sdsdataStoreRecId)
{
// Populate table RetailCDXDownloadSession

ttsBegin;
retailCDXDownloadSession.clear();
retailCDXDownloadSession.initValue();
if (sDataFileOutputPath)
{
retailCDXDownloadSession.DataFileOutputPath = sDataFileOutputPath;
}
retailCDXDownloadSession.DataGroup = sdatagroupRecId;
retailCDXDownloadSession.JobID = sjobId;
if (srowsAffected > 0)
{
retailCDXDownloadSession.RowsAffected = srowsaffected;
}
if (sscheduleRecId)
{
retailCDXDownloadSession.Schedule = sscheduleRecId;
}
retailCDXDownloadSession.Session = ssessionNumber;
if (sdownloadStatus)
{
retailCDXDownloadSession.Status = sdownloadStatus;
}
retailCDXDownloadSession.insert();
sessionRecId = retailCDXDownloadSession.RecId;
ttsCommit;
// Populate table retailCDXDownloadSessionDataStore

ttsBegin;
retailCDXDownloadSessionDataStore.clear();
retailCDXDownloadSessionDataStore.initValue();
if (sdslinkedSessionRecId)
{
retailCDXDownloadSessionDataStore.pmoLinkedSession = sdslinkedSessionRecId;
}
retailCDXDownloadSessionDataStore.DataStore = sdsdataStoreRecId;
if (sdsdateApplied)
{
sdsdateAppliedUTC = str2datetime(sdsdateApplied,123);
retailCDXDownloadSessionDataStore.DateApplied = sdsdateAppliedUTC;
}
retailCDXDownloadSessionDataStore.Session = sessionRecId;
if (sdsdateDownloaded)
{
sdsdateDownloadedUTC = str2datetime(sdsdateDownloaded,123);
retailCDXDownloadSessionDataStore.DateDownloaded = sdsdateDownloadedUTC;
}
if (sdsdateRequested)
{
sdsdateRequestedUTC = str2datetime(sdsdateRequested,123);
retailCDXDownloadSessionDataStore.dateRequested = sdsdateRequestedUTC;
}
if (sdsmessage)
{
retailCDXDownloadSessionDataStore.Message = sdsmessage;
}
if (sdsdownloadStatus)
{
retailCDXDownloadSessionDataStore.Status = sdsdownloadStatus;
}
if (uDataSource)
{
retailCDXDownloadSessionDataStore.pmoDataSource = uDataSource;
}
retailCDXDownloadSessionDataStore.insert();
ttsCommit;
success = true;
}
else
{
error = ‘@PMO17’;
success = false;
}
}
else
{
// If SessionNumber is 0 then create a new session

if (uUploadSessionId == 0)
{
// The upload session is synched from AsyncServerHeadOffice\dbo.UPLOADSESSION, so a new session ID should not interfere with the SQL ID range
uUploadSessionId = RetailCDXUploadSession::getNextBizTalkSession();
}
if (uUploadSessionId > 0 && uJobId && uDataStoreRecId)
{
// Populate table RetailCDXUploadSession

ttsBegin;
retailCDXUploadSession.clear();
retailCDXUploadSession.initValue();
if (uUploadPath)
{
retailCDXUploadSession.UploadPath = uUploadPath;
}
retailCDXUploadSession.DataStore = uDataStoreRecId;
retailCDXUploadSession.JobId = uJobId;
if (uRowsAffected > 0)
{
retailCDXUploadSession.RowsAffected = uRowsAffected;
}
if (uScheduleRecId)
{
retailCDXUploadSession.Schedule = uScheduleRecId;
}
retailCDXUploadSession.UploadSessionId = uUploadSessionId;
if (uUploadStatus)
{
retailCDXUploadSession.Status = uUploadStatus;
}
if (uCheckSum)
{
retailCDXUploadSession.CheckSum = uCheckSum;
}
if (uDateCreated)
{
uDateCreatedUTC = str2datetime(uDateCreated,123);
retailCDXUploadSession.DateCreated = uDateCreatedUTC;
}
if (uDateUploaded)
{
uDateUploadedUTC = str2datetime(uDateUploaded,123);
retailCDXUploadSession.DateUploaded = uDateUploadedUTC;
}
if (uFileSize)
{
retailCDXUploadSession.FileSize = uFileSize;
}
if (uHQUploadSessionId)
{
retailCDXUploadSession.HqUploadSessionId = uHQUploadSessionId;
}
if (uMessage)
{
retailCDXUploadSession.Message = uMessage;
}
if (uTryCount > 0)
{
retailCDXUploadSession.TryCount= uTryCount;
}
if (uRerun > 0)
{
retailCDXUploadSession.Rerun= uRerun;
}
if (uRerunForSessionId)
{
retailCDXUploadSession.RerunFor = uRerunForSessionId;
}
if (uDataSource)
{
retailCDXUploadSession.pmoDataSource = uDataSource;
}
retailCDXUploadSession.insert();
ttsCommit;
// Populate table RetailCDXUploadSessionLog

ttsBegin;
retailCDXUploadSessionLog.clear();
retailCDXUploadSessionLog.initValue();
retailCDXUploadSessionLog.DataStore = uDataStoreRecId;
if (uDateCreated)
{
uDateCreatedUTC = str2datetime(uDateCreated,123);
retailCDXUploadSessionLog.DateCreated = uDateCreatedUTC;
}
if (uMessage)
{
retailCDXUploadSessionLog.Message = uMessage;
}
if (uUploadStatus)
{
retailCDXUploadSessionLog.Status = uUploadStatus;
}
retailCDXUploadSessionLog.UploadSessionId = uUploadSessionId;
retailCDXUploadSessionLog.insert();
ttsCommit;
success = true;
}
else
{
error = ‘@PMO17’;
success = false;
}
}
}
catch(Exception::Error)
{
ttsabort;
error = RetailTransactionService::getInfologMessages(fromLine);
RetailTracer::Error(‘RetailTransactionServiceEx’,funcName(),error);
success = false;
}
tmpResult = [success, error];
return tmpResult;
}

Enhancements to the Download session and Upload session form

In the Upload session form, the Message field is not visible by default. As this field will contain any positive or negative feedback from our middleware or 3rd party system, this is the first thing to change:

Microsoft Dynamics AX Retail - Upload sessions customization.jpg
As shown in the picture, in both Upload and Download session form I’ve added the Origin field which indicates which link of the interface chain is reporting to us (see second column). Last but not least I’ve added a Linked download session field to the Download session form which shows which session (read: record in the Download session form) the current record followed up on. In this way, you can easy filter the form to have all the meta data as reported by the different chains of the complete interface flow in one overview.
I hope this has shown you how powerful Dynamics AX for Retail can also be leveraged for 3rd party sales channel integrations.

Happy DAX’ing!
Patrick