In Part 1 of this series, I discussed how to code a dll in C# which would create a retail sales order using the Retail Proxy. In Part 2 below, I will show how to use this dll in X++ code. We will create a REST service which will be called from our integration to our third-party ecommerce system.
In your X++ Visual Studio project, add a reference to the RetailProxy dll and the C# project we created in Part 1.
An AxReference folder will be added to your model and the dll will be copied to the bin folder of your package. For example, if your model and package are called RetailProxyTest then you will see the following elements created:
· The assemblies will be copied to PackagesLocalDirectory\ RetailProxyTest \bin\
· An AXReference folder and a file named Thirdpartyassembly.xml are created in PackagesLocalDirectory\ RetailProxyTest \ RetailProxyTest \AxReference\Thirdpartyassembly.xml
Remember to commit these files to source control, otherwise the modification will not work when it is deployed elsewhere.
You can download the full code here. I will explain the different parts of the code below.
The createSalesOrder method is the entry point for our service.
[AifCollectionTypeAttribute("_shippingAddress", Types::Class,
classStr(I2I_ShippingAddressMessage)),
AifCollectionTypeAttribute("_cartLines", Types::Class,
classStr(I2I_SalesLineMessage))]
public void createSalesOrder(str _salesId, str _email, str _custId,
str _currency, str _deliveryDate,
str _shipDate, str _modeOfDelivery,
str _opUnitNum, str _shippingWarehouseId,
str _shipLocation, List _shippingAddress,
List _cartLines)
The I2I_ShippingAddressMessage and I2I_SalesLineMessage classes are the object representations of the shipping address and sales lines json messages.
First, we query the retailChannelTable table to get the dataAreaId and timeZone for the operating unit that is passed in.
select firstonly
RecId, inventLocationDataAreaId, ChannelTimeZone
from retailChannelTable
exists join omOperatingUnit
where retailChannelTable.OMOperatingUnitID == omOperatingUnit.RecId
&& omOperatingUnit.OMOperatingUnitNumber == _opUnitNum
&& omOperatingUnit.OMOperatingUnitType ==
OMOperatingUnitType::RetailChannel;
I have a private method called createRetailAddress to create the shipping address that is required on the cart line.
private static Microsoft.Dynamics.Commerce.RetailProxy.Address createRetailAddress(List _shippingAddress, TaxGroup _taxGroup,
CustName _name)
{
Microsoft.Dynamics.Commerce.RetailProxy.Address retailAddr =
new Microsoft.Dynamics.Commerce.RetailProxy.Address();
ListIterator iter = new ListIterator(_shippingAddress);
if (iter.more())
{
I2I_ShippingAddressMessage address = iter.value();
retailAddr.ThreeLetterISORegionName = address.Country();
retailAddr.ZipCode = address.PostCode();
retailAddr.City = address.City();
retailAddr.Street = address.Street();
retailAddr.State = address.Province();
retailAddr.TaxGroup = _taxGroup;
retailAddr.AddressTypeValue = AddressType::Delivery;
retailAddr.Name = substr(_name, 0, 60) ;
str fullAddress = address.Street() + '\n' +
address.City() + ', ' +
address.Province() + '\n' +
address.PostCode() + '\n' +
address.Country();
retailAddr.FullAddress = fullAddress;
}
return retailAddr;
}
There is another private method called createRetailCartLine to create a cart line.
private static Microsoft.Dynamics.Commerce.RetailProxy.CartLine createRetailCartLine(I2I_SalesLineMessage _salesLineMessage,
Microsoft.Dynamics.Commerce.RetailProxy.Address _retailAddr,
InventLocationId _shippingWarehouseId, WMSLocationId
_shipLocation)
{
Microsoft.Dynamics.Commerce.RetailProxy.CartLine retailCartLine
= new Microsoft.Dynamics.Commerce.RetailProxy.CartLine();
EcoResProduct ecoResProd =
EcoResProduct::findByDisplayProductNumber
(_salesLineMessage.ItemId());
if (!ecoResProd.RecId)
{
throw Error(strfmt("ItemId %1 not found.",
_salesLineMessage.ItemId()));
}
retailCartLine.ProductId = ecoResProd.RecId;
retailCartLine.ItemId = _salesLineMessage.ItemId();
retailCartLine.Quantity = _salesLineMessage.Qty();
retailCartLine.UnitOfMeasureSymbol =
_salesLineMessage.UnitofMeasure();
retailCartLine.SerialNumber = _salesLineMessage.SerialId();
retailCartLine.ShippingAddress = _retailaddr;
return retailCartLine;
}
When creating a payment, the retail proxy requires a service account id, card token, and unique card id. The service account id is from the test payment connector. As I mentioned in Part 1, I do not want to create another real payment since the payment was already taken with our third party e-commerce platform. Hence, we are using the test payment connector here.
I saved the service account id in a new customer parameter and am retrieving it with the following:
//get service account Id and cardToken
CustParameters custParam = CustParameters::find();
str serviceAccountId = custParam.I2I_ServiceAccountId;
I pre-created the payment card under the default online store customer. I used the test credit card number of "4111111111111111" and entered a random address.
After we have created the payment card in D365FO, we can query to retrieve the card token and unique card id. I put a custom flag on the creditCartCust table called I2I_ShipmentsInterfaceCard to identify which card to use. You may not need to do this if there is only one card that you use.
CreditCardCust creditCardCust;
//We are using a saved card token. This card must be pre-created in
//D365.
select firstOnly CardToken, UniqueCardId from creditCardCust
where creditCardCust.I2I_ShipmentsInterfaceCard == NoYes::Yes;
We now are able to call the dll we created in Part 1. Notice that I had to convert the utcDateTime delivery date to a System.DateTime type. I save the transaction id returned into a variable which we will use later.
///
/// CREATE SALES ORDER VIA RETAIL SERVER
///
str transId;
utcdatetime deliveryDate =
DateTimeUtil::newDateTime(str2Date(_deliveryDate, 321), 0,
retailChannelTable.ChannelTimeZone);
System.DateTime dotNetDeliveryDate =
Global::utcDateTime2SystemDateTime(deliveryDate);
I2i.RetailProxyDemo.RetailServerLibrary rsLibrary =
new I2i.RetailProxyDemo.RetailServerLibrary();
transId = rsLibrary.createSalesOrder(retailServerUrl, _opUnitNum,
_email, _currency, _modeOfDelivery, dotNetDeliveryDate,
retailCartLines, serviceAccountId,
creditCardCust.CardToken, creditCardCust.UniqueCardId);
As I mentioned in Part 1, normally an online sales order is created with the actual customer logged onto a website. Since this is an integration scenario, there is no customer authenticated so we must use the retail proxy with anonymous authentication. The problem with this is that all the online sales orders will be created with the default customer. I would like the sales order to be created with the customer id that I am passing into the service. We will use SQL to update the customer account on the retail transaction table.
///
/// UPDATE THE CUSTOMER ID FROM THE DEFAULT ONLINE CUSTOMER
///
Connection conn = new Connection();
Statement statement = conn.createStatement();
str sql = @"Update ax.RetailTransactionTable set CustAccount = '%1',
TransDate = '%2'
where transactionId = '%3' and dataAreaId = '%4'
and channel = '%5' and terminal='' and store=''";
sql = strFmt(sql, _custId, _shipDate, transId,
retailChannelTable.inventLocationDataAreaId,
retailChannelTable.RecId);
new SqlStatementExecutePermission(sql).assert();
statement.executeUpdate(sql);
We will do something similar with the payment records. The payments from the third party e-commerce system are flowing to D365FO through a separate interface. We do not want this sales order transaction to generate another payment record. Therefore, we will use SQL to update the payment transaction record amounts to zero.
///
/// UPDATE PAYMENT AMOUNTS TO ZERO
///
sql = @"Update ax.RetailTransactionPaymentTrans set AMOUNTCUR=0,
AMOUNTMST=0, AMOUNTTENDERED=0
where transactionId = '%1' and dataAreaId = '%2' and
channel = '%3' and terminal='' and store=''";
sql = strFmt(sql, transId, retailChannelTable.inventLocationDataAreaId,
retailChannelTable.RecId);
new SqlStatementExecutePermission(sql).assert();
statement.executeUpdate(sql);
At this point, you should be able synchronize your online orders and see them in D365FO.
A note about catching exceptions
Use the following code to catch exceptions from managed code and return a meaningful error message back to the caller:
catch(Exception::CLRError)
{
System.Exception ex = ClrInterop::getLastException();
if (ex != null)
{
ex = ex.get_InnerException();
if (ex != null)
{
throw Error(ex.ToString());
}
}
}
Comments