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):
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:
Message for calling the Real Time Service (newCustomer method) = The “sender”
AX 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’:
- 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):
- 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.
- 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.
/// <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:
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
Dynamics AX Retail 3rd party POS and E-commerce integration – Solution Design - Microsoft Dynamics AX Community
oktober 21, 2015 @ 20:32
[…] links up till its destination and easily intervene in case of any issues! See my blog post Control dashboard for more details if you’re interested in this feature. So let’s get back to our main […]
Dynamics AX Retail 3rd party POS and E-commerce integration – Introduction - Microsoft Dynamics AX Community
oktober 21, 2015 @ 20:32
[…] Retail 3rd party integration – Automate process AND data exchange (blog post will follow later) Dynamics AX Retail 3rd party integration – Control dashboard Dynamics AX Retail 3rd party integration – Agility (blog post will follow […]
Waldemar Pross
november 27, 2015 @ 18:06
Very nice. Will start working through it soon and let you know how it works out for us (we are using DemandWare as eCommerce).
Waldemar
Waldemar Pross
november 29, 2015 @ 09:50
Hi Patrick.
How do you actually call the Real Time Service? Is there a WSDL like you can get for AIF ports (or typically for any other SOAP service)?
Or otherwise how do you know the URL and format of the XML you have to put in your request to RTS?
We have the Real Time Service up and running in our test environment with physical POS connected and I can see the RTS (and Async) in the IIS, but still struggle to understand where to start.
Thanks!
Waldemar
Patrick Mouwen
november 30, 2015 @ 22:28
Hi Waldemar,
To go short as there’s a lot to say on your questions: I intercepted all XML traffic generated by POS operations by adding a listener for logging in highest detail to the web.config file. Then we simulated the exact same messages with BizTalk.
See here (https://onedrive.live.com/redir?resid=79255561C5EDE5D5!24241&authkey=!AJ9Et3XdC2bqERU&ithint=file%2cxlsx) a sample for the GetCustomerOrder method which you’ll find in RetailTransactionServiceOrders∷getCustomerOrder in AX2012R3. It’s all about the yellow highlighted part: The RequestInfo part can be regarded as the header of the message containing basic meta data, followed by the method name and the parameters. If you look into the method in AX you’ll find these parameters ‘on the other side’ ;-).
See further here (https://onedrive.live.com/redir?resid=79255561C5EDE5D5!24243&authkey=!AP6Ac-G9OipLVT4&ithint=folder%2c) a short description I created on how to get the WSDL for RTS.
I hope this helps.
Patrick
Waldemar Pross
december 2, 2015 @ 14:10
Hi Patrick. Thanks a lot for this information.
It turns out to be not that bad I feared: originally I also thought that in worst case I will have to look into the communication using tools like Fiddler or Wireshark.
But if I can get the WSDL out, this might simplify things. I will try and feed back here.
At the end, it still feels like this is not the “as designed by Microsoft” way, which brings me to related question…
If I understood correctly, the “official” way how you can integrate is via CRT. I haven’t tried myself yet, but it sounds like CRT has services for both, real-time and asynchronous exchange.
I don’t think it would work out-of-the box, but maybe it is possible to build some layer in .NET to expose them all as normal SOAP services via HTTP.
I am not sure how the standard Fabrikam SP based shop is calling it, but I am quite sure that it is something non-Windows clients or software like BizTalk can call straightaway.
What is yur experience with CRT? Are you using it at all to integrate Hybris via BizTalk?
Thanks!
Waldemar
Waldemar Pross
december 2, 2015 @ 18:45
Hi Patrick.
I managed to generate the WSDL (thanks for the instruction again!) and now trying to write a simple client. Added the WSDL as reference to Visual Studio and then tried to follow this Microsoft whitepaper on how to extend Real Time services (I wanted to create a new simple method to test):
http://www.microsoft.com/en-us/download/details.aspx?id=39091
Unfortunately it describes how to call the new method from the “old” Retail POS.
The WSDL has generated slightly different classes / methods, so I keep digging.
Shouldn’t be that hard now.
Regarding CRT: if you look at this diagram:
http://blogs.msdn.com/cfs-filesystemfile.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-53-11/4478.Topology-Central.png
My idea is to simply call what is the orange layer on this diagram (ODATA/REST). It feels like those are simple REST interfaces and should much easier to call from non-Microsoft middleware like our ESB “Mule”.
But this seems to be valid only for the “Modern POS” and unfortuantelywe are using the “old” Retail POS (aka ePOS).
But I was thinking, since the HQ side is the same no matter which POS you use, I can install the modern Retail Server on the same machine as our ESB, so I can call the ODATA/REST interfaces.
No more questions for now. Just wanted to share my findings with you 🙂