• Davie Lau

Part 1 - Create a D365 Retail Sales Order Integration through Commerce Runtime and Retail Proxy

Updated: Jun 9, 2020


Create a d365 sales order integration title image

Overview

One of our clients wanted to send sales orders from a third-party e-commerce system into Dynamics 365 for Finance and Operations (D365FO). The client also operated retail stores and were interested in potentially using D365 Modern Point of Sale (MPOS) in the future. We wanted to design this integration to utilize the Commerce Runtime engine to create retail sales orders, instead of directly creating regular non-retail sales orders. This way, when the client does eventually start using MPOS, they can take advantage of the omni-channel capabilities that D365 Retail can deliver.


How We Did It

Microsoft provides a way of accessing the CRT functionality through the retail proxy. We will create a class library in C# and then use this library in X++ code.


Open Visual Studio and create a new C# Class library project.



In your new solution, right click References, and add a reference to Microsoft.Dynamics.Commerce.RetailProxy.dll which is located in path similar to here: K:\RetailSDK\References\Microsoft.Dynamics.Commerce.RetailProxy.9.19.20030.24\lib\net461.

You should now see the reference to the Retail Proxy in your solution.
















Add a new class to the solution. Below is the complete code but I will break down each section for you. The parameters of this method contain all the data that I need to create my shopping cart and to check out the order. In a later post, I will show in X++, how to create the cart lines and retrieve the rest of the values for the parameters.

using System;
using System.Collections.Generic;
using Microsoft.Dynamics.Commerce.RetailProxy;

namespace I2i.RetailProxyDemo
{   
    public class RetailServerLibrary
    {
        /// <summary>
        /// Create retail sales order through retail server
        /// </summary>        
        /// <param name="retailServerUrl"></param>
        /// <param name="operatingUnitNumber"></param>      
        /// <param name="email"></param>
        /// <param name="customerId"></param>
        /// <param name="cartLines"></param>
        /// <param name="cartTenderLines"></param>
        /// <param name="cardToken"></param>
        /// <param name="uniqueCardId"></param>
        /// <returns>transaction id</returns>
        public string createSalesOrder(string retailServerUrl, 
             string operatingUnitNumber, string email, 
             string currency, string modeOfDelivery, 
             DateTime reqDeliveryDate, CartLine[] cartLines, 
             string serviceAccountId, string cardToken, 
             string uniqueCardId)
        {          
            RetailServerContext context = RetailServerContext.Create(
                new Uri(retailServerUrl),
                operatingUnitNumber);

            ///
            /// CREATE NEW CART
            ///
            ManagerFactory managerFactory = 
                ManagerFactory.Create(context);              
            ICartManager cartManager = 
                managerFactory.GetManager<ICartManager>();
            Cart cart = null;

            cart = AsyncHelpers.RunSync<Cart>(() => 
              cartManager.Create(new Cart() {                 
                Id = String.Empty, 
                CartTypeValue = (int)CartType.Checkout,
                DeliveryMode = modeOfDelivery,
                RequestedDeliveryDate = reqDeliveryDate,           
                Comment = String.Empty,
                ShippingAddress = cartLines[0].ShippingAddress         
            }));
            
            cart = AsyncHelpers.RunSync<Cart>(() => 
               cartManager.AddCartLines(cart.Id, cartLines, null));
            
            
            ///
            /// CREATE NEW PAYMENT CARD
            ///
            CardTokenInfo cardTokenInfo = new CardTokenInfo();            
            cardTokenInfo.MaskedCardNumber = "************1111";         
            cardTokenInfo.UniqueCardId = uniqueCardId;
            cardTokenInfo.ServiceAccountId = serviceAccountId;
            cardTokenInfo.CardToken = cardToken;

            CartTenderLine[] retailTenderLines = new CartTenderLine[1];           
            TokenizedPaymentCard payCard = new TokenizedPaymentCard();
            
            payCard.CardTypeId = "Visa";
            payCard.Address1 = cartLines[0].ShippingAddress.Street;
            payCard.City = cartLines[0].ShippingAddress.City;
            payCard.Zip = cartLines[0].ShippingAddress.ZipCode;
            payCard.Country = cartLines[0].ShippingAddress.ThreeLetterISORegionName;
            payCard.NameOnCard = "Dummy card";
            payCard.ExpirationMonth = 2;
            payCard.ExpirationYear = 2099;
            payCard.State = cartLines[0].ShippingAddress.State;
            payCard.CardTokenInfo = cardTokenInfo;


            ///
            /// CREATE THE PAYMENT
            ///            
            CartTenderLine retailCartTenderLine = new CartTenderLine();
            retailCartTenderLine.Currency = currency;            
            retailCartTenderLine.TenderTypeId = "2";  //Visa card
            retailCartTenderLine.CardTypeId = "Visa";
            retailCartTenderLine.TokenizedPaymentCard = payCard;            
            retailCartTenderLine.Amount = cart.AmountDue;
          
            List<CartTenderLine> cartTenderLines 
                = new List<CartTenderLine>();
            cartTenderLines.Add(retailCartTenderLine);
            
            ///
            /// CHECKOUT THE ORDER
            ///
            SalesOrder salesOrder = AsyncHelpers.RunSync<SalesOrder>
              (() => cartManager.Checkout(
               id: cart.Id,
               receiptEmail: email,
               tokenizedPaymentCard: null,
               receiptNumberSequence: null,
               cartTenderLines: cartTenderLines,                             
               cartVersion: null                                                          
               ));
           
            return cart.Id;
        } 
    }     
}

The following code is used to create the cart:

///
/// CREATE NEW CART
///
ManagerFactory managerFactory = ManagerFactory.Create(context);              
ICartManager cartManager = managerFactory.GetManager<ICartManager>();
Cart cart = null;

cart = AsyncHelpers.RunSync<Cart>(() => cartManager.Create(new Cart() {                 
    Id = String.Empty, 
    CartTypeValue = (int)CartType.Checkout,
    DeliveryMode = modeOfDelivery,
    RequestedDeliveryDate = reqDeliveryDate,           
    Comment = String.Empty,
    ShippingAddress = cartLines[0].ShippingAddress         
}));

cart = AsyncHelpers.RunSync<Cart>(() => cartManager.AddCartLines(cart.Id, cartLines, null));

The retail proxy methods are asynchronous but I need to call them synchronously. I’m using the AsyncHelpers class to do this. The code is from this link: https://gist.github.com/ChrisMcKee/6664438. I've also copied the class to my OneDrive here: https://bit.ly/3cPzpQ7. Let’s copy this code and add it to our solution.


Next, we need to create the payment. Our third-party e-commerce system has already accepted the payment and the payments are flowing to Dynamics through a separate interface. We do not want Dynamics to create another payment record, but a payment is required to complete the transaction. Therefore, we configured our online store to use the test payment connector. This will allow us to checkout without creating a real payment.

Now we need to create a payment card. Since we are using the test connector, we can really enter any bogus data for the card number and address. In my example, I am actually using the customer’s real shipping address, but you could use any fake address.

///
/// CREATE NEW PAYMENT CARD
///
CardTokenInfo cardTokenInfo = new CardTokenInfo();            
cardTokenInfo.MaskedCardNumber = "************1111";         
cardTokenInfo.UniqueCardId = uniqueCardId;
cardTokenInfo.ServiceAccountId = serviceAccountId;
cardTokenInfo.CardToken = cardToken;

CartTenderLine[] retailTenderLines = new CartTenderLine[1];           
TokenizedPaymentCard payCard = new TokenizedPaymentCard();
            
payCard.CardTypeId = "Visa";
payCard.Address1 = cartLines[0].ShippingAddress.Street;
payCard.City = cartLines[0].ShippingAddress.City;
payCard.Zip = cartLines[0].ShippingAddress.ZipCode;
payCard.Country = cartLines[0].ShippingAddress.ThreeLetterISORegionName;
payCard.NameOnCard = "Dummy card";
payCard.ExpirationMonth = 2;
payCard.ExpirationYear = 2099;
payCard.State = cartLines[0].ShippingAddress.State;
payCard.CardTokenInfo = cardTokenInfo;

Now we create the tender line using the payment card we just created. We set the tender line amount to the total amount of the cart.

///
/// CREATE THE PAYMENT
///            
CartTenderLine retailCartTenderLine = new CartTenderLine();
retailCartTenderLine.Currency = currency;            
retailCartTenderLine.TenderTypeId = "2";  //Visa card
retailCartTenderLine.CardTypeId = "Visa";
retailCartTenderLine.TokenizedPaymentCard = payCard;            
retailCartTenderLine.Amount = cart.AmountDue;

List<CartTenderLine> cartTenderLines 
    = new List<CartTenderLine>();
cartTenderLines.Add(retailCartTenderLine);

Finally, we check out the order and return the transaction id. At this point, the retail transaction would have been created inside the channel database.

///
/// CHECKOUT THE ORDER
///
SalesOrder salesOrder = AsyncHelpers.RunSync<SalesOrder>
    (() => cartManager.Checkout(
    id: cart.Id,
    receiptEmail: email,
    tokenizedPaymentCard: null,
    receiptNumberSequence: null,
    cartTenderLines: cartTenderLines,                             
    cartVersion: null                                                          
    ));

return cart.Id;

Customer and authentication

In a normal online store transaction, the customer would need to login and authenticate. The order would then be placed under this customer’s account. Since this is an integration scenario, the concept of having the user login does not apply. To get the above code to work, we need to enable anonymous access. To do this, edit the retail server’s web.config file located here: K:\RetailServer\webroot\web.config. Make sure the "IsAnonymousEnabled” value is set to true.


To enable this on a production system, you must create a ticket with MS Support.

But what customer gets used with the order if we enable anonymous authentication? It uses the default customer of the online store. Every single order will get the same customer. This is not what we want though. In my next post, I will show you how to update the customer on the order and how to call the dll in X++.

1,249 views