ANEXO I: Entrevista a Ana Mª Álvarez Mesas, Responsable de Marketing de la Liga Nacional de Fútbol Sala 27 de abril de 2018 Oficinas de la LNFS Madrid.
P. En referencia al tema de los precios, ¿Cree que eso ha sido una de las claves del éxito?
To update the properties that are available in the commerce entity, you must do the following:
1. Identify the field in Microsoft Dynamics AX that you want to expose in the commerce entity. If the data that you want is not in an existing table, you must create a new field on a new or existing table. 2. Change the stored procedure or view that specifies the data that is
passed from Microsoft Dynamics AX to the commerce entity. 3. Update the commerce entity to include the new field.
Note: If the field that you want to use already exists, go to ”To change the view to include the new field.”
4. Add the new field in the CRT database.
Customization
To change the view to include the new field, do the following:
1. Connect to your database server in SQL Server Management Studio. 2. Expand the Databases node, the node for your CRT database and the
Views node, and then find the view that corresponds to the data entity you want to change.
3. Right-click the view, point to Script View as, point to ALTER To, and then click New Query Editor Window.
4. Update the view to include the field that you created. To update the commerce entity, do the following:
1. Open the commerce entity in Visual Studio.
2. Add a string variable to represent the property that you are adding, for example, see the following:
private const string CustAge = “AGE”;
3. Add a method to cast the value from the field in Microsoft Dynamics AX as a string and set it as the value for the property, for example, see the following:
public string Age {
get { return (string)this[CustAge]; } set { this[CustAge] = value; }
}
Customize Workflows in Commerce Runtime
The commerce runtime includes workflow business logic that enforces rules for your business. For example, after a customer places an order, you can use a workflow to make sure that there is sufficient quantity available, validate the tax calculation, check for credit approval, and then place the order. You can use the workflows that are included in CRT or create your own.
A service encapsulates operations to perform one atomic task, whereas a workflow performs a set of tasks. A workflow contains a set of steps that calls into one or more services.
To create a workflow, do the following:
Exactly like services, workflows use the request and response pattern. The request object inherits from the base commerce runtime Request class. The response object inherits from the base commerce runtime Response class.
A workflow also has a request handler class that extends the
WorkflowRequestHandler<TRequest, TResponse> class. To create a workflow, you create a request class and a response class, and then create a request handler class that contains the business logic for your workflow. The following is an example workflow for managing promotions.
To create a request, do the following:
Create a class that inherits from the base commerce runtime Request class, and create any incoming parameters that you must have for your workflow. The following example shows the GetPromotionsRequest class from the Promotions workflow.
namespace Extensions.Promotions.Workflow {
using Microsoft.Dynamics.Commerce.Runtime.Messages; /// <summary>
/// Request Object for the PromotionsWorkFlowRequestHandler class. /// </summary>
public class GetPromotionsRequest : Request
{
/// <summary>
/// Initializes a new instance of the <see cref="GetPromotionsRequest"/> class. /// </summary>
public GetPromotionsRequest() {
}
/// <summary>
/// Gets or sets the channel identifier. /// </summary>
public long ChannelId { get; set; } /// <summary>
/// Gets or sets the catalog identifier. /// </summary>
public long CatalogId { get; set; } /// <summary>
/// Gets or sets a value indicating whether to get the summarized data set. /// </summary>
public bool HasPromotionData { get; set; } }
}
To create a response, do the following:
Create a class that inherits from the base commerce runtime Response class. The following example displays the GetPromotionsResponse class from the
Promotions workflow.
namespace Extensions.Promotions.Workflow {
using System.Collections.Generic; using System.Collections.ObjectModel; using Extensions.Promotions.Data;
using Microsoft.Dynamics.Commerce.Runtime;
using Microsoft.Dynamics.Commerce.Runtime.Messages; /// <summary>
/// Creates the response object for the Workflow. /// </summary>
public class GetPromotionsResponse : Response
{
/// <summary>
/// Initializes a new instance of the <see cref="GetPromotionsResponse"/> class. /// </summary>
/// <param name="promotions">The summerized promotions.</param>
Customization
{this.SummarizedPromotions = promotions.AsReadOnly(); }
/// <summary>
/// Initializes a new instance of the <see cref="GetPromotionsResponse"/> class. /// </summary>
/// <param name="promotions">The promotions.</param>
public GetPromotionsResponse(IEnumerable<PromotionDetails> promotions) {
this.Promotions = promotions.AsReadOnly(); }
/// <summary>
/// Gets all the summarized channel promotions. /// </summary>
public ReadOnlyCollection<ChannelPromotionsSummarizedData> SummarizedPromotions {
get; private set; } /// <summary>
/// Gets non-summarized channel promotions. /// </summary>
public ReadOnlyCollection<PromotionDetails> Promotions { get; private set; } }
}
To create a request handler, do the following:
Create a class that inherits from the base commerce runtime
WorkflowRequestHandler<TRequest, TResponse> class. The following example shows the GetPromotionsResponse class from the Promotions workflow.
namespace Extensions.Promotions.Workflow {
using System;
using System.Collections.Generic; using System.Linq;
using Extensions.Promotions.Data; using Extensions.Promotions.Service;
using Microsoft.Dynamics.Commerce.Runtime.DataModel; using Microsoft.Dynamics.Commerce.Runtime.Workflow;
using Microsoft.Dynamics.Commerce.Runtime.Workflow.Composition; /// <summary>
/// Helper Class to call neccessary Services and perform Data Manipulation on it. /// </summary>
[RequestHandlerMetadata(HandledRequestType = typeof(GetPromotionsRequest))] internal class PromotionsWorkflowRequestHandlerRaw :
WorkflowRequestHandler<GetPromotionsRequest, GetPromotionsResponse> {
/// <summary>
/// Executes the workflow to get the promotions. /// </summary>
/// <param name="request">The request.</param> /// <returns>The response.</returns>
protected override GetPromotionsResponse Process(GetPromotionsRequest request) {
if (request == null) {
throw new ArgumentNullException("request"); }
var serviceRequest = new GetPromotionsServiceRequest(this.Context);
IPromotions promotionsService = this.Context.Runtime.GetService<IPromotions>(); List<PromotionDetails> response = new List<PromotionDetails>();
List<ChannelPromotionsSummarizedData> summarizedResponse = new
List<ChannelPromotionsSummarizedData>();
serviceRequest.ChannelId = request.ChannelId; serviceRequest.CatalogId = request.CatalogId; foreach (PeriodicDiscountOfferType discount in
Enum.GetValues(typeof(PeriodicDiscountOfferType))) {
serviceRequest.PromotionType = discount; var serviceReply =
promotionsService.Execute<GetPromotionsServiceResponse>(serviceRequest).Promotions; if (request.HasPromotionData) { summarizedResponse.AddRange(ConvertToSummarizedData(serviceReply, discount)); } else { response.AddRange(serviceReply); } } if (request.HasPromotionData) {
var summarizedResult = from s in summarizedResponse orderby s.DiscountName
select s;
return new GetPromotionsResponse(summarizedResult); }
else
{
var result = from s in response orderby s.DiscountName
select s;
return new GetPromotionsResponse(result); }
}
/// <summary>
/// Iterates through a list of promotions for:
/// 1. Generating a list of discount codes associated with an Item. This generates duplicate records where the only difference is the discount codes.
/// 2. Removing above duplicates.
/// 3. Summarize the Promotion data into English Text for every unique record being considered.
/// </summary>
/// <param name="promotionDetailsCollection">List containing promotion details.</param>
/// <param name="typeOfDiscount">Discount Type associated with the list (eg. MultiBuy/ MixNMatch, etc).</param>
/// <returns>A collection of summarized promotions.</returns> private static List<ChannelPromotionsSummarizedData>
ConvertToSummarizedData(IEnumerable<PromotionDetails> promotionDetailsCollection,
PeriodicDiscountOfferType typeOfDiscount) {
var returnObject = new List<ChannelPromotionsSummarizedData>(); var allDiscountOfferNames = promotionDetailsCollection
.Select(i => new { i.DiscountName }) .Distinct();
foreach (var currentOffer in allDiscountOfferNames) {
var discountLineItems = from s in promotionDetailsCollection where
s.DiscountName == currentOffer.DiscountName orderby s.ItemId select s;
var lineItemsAfterPass1 = Pass1ConsolidateDiscountCodes(discountLineItems); var lineItemsAfterPass2 =
Customization
var lineItemsAfterPass3 = Pass3GenerateConsolidatedPromotionStatement(lineItemsAfterPass2, typeOfDiscount); returnObject.AddRange(lineItemsAfterPass3); } return returnObject; } /// <summary>/// Consolidate all the discount codes available for a discount and then remove redundant line items.
/// </summary>
/// <param name="offerCollection">Contains set of all Line Items for a particular discount Offer.</param>
/// <returns>A collection of promotion details..</returns> private static List<PromotionDetails>
Pass1ConsolidateDiscountCodes(IEnumerable<PromotionDetails> offerCollection) {
var returnObject = new List<PromotionDetails>(); var element = new PromotionDetails();
var iterator = offerCollection.ToList();
for (int counter = 0; counter < offerCollection.Count(); counter++) {
element = iterator.ElementAt(counter); if (element.IsDiscountCodeRequired) {
int index = 1;
while (counter + index < offerCollection.Count() &&
iterator.ElementAt(counter + index).ItemId == element.ItemId && iterator.ElementAt(counter + index).IsDiscountCodeRequired == element.IsDiscountCodeRequired)
{
element.DiscountCode += ", " + iterator.ElementAt(counter + index).DiscountCode; index++; } counter += index; } returnObject.Add(element); } return returnObject; } /// <summary>
/// Generates a Summarized Promotion Statement for every line item, But it is still not usable by the End-User.
/// </summary>
/// <param name="offerCollection">Contains set of all Line Items for a particular discount Offer.</param>
/// <param name="typeOfDiscount">Is it MultiBuy/ Mix-n-Match/ Discount/ Price Adjustment.</param>
/// <returns>A collection of summarized promotions.</returns> private static List<ChannelPromotionsSummarizedData>
Pass2GenerateSummarizedPromotionStatement(IEnumerable<PromotionDetails> offerCollection,
PeriodicDiscountOfferType typeOfDiscount) {
var returnObject = new List<ChannelPromotionsSummarizedData>(); ChannelPromotionsSummarizedData element;
foreach (var lineItem in offerCollection) {
element = new ChannelPromotionsSummarizedData(); element.OfferId = lineItem.OfferId;
element.DiscountName = lineItem.DiscountName;
element.DiscountDescription = lineItem.DiscountDescription; element.Disclaimer = lineItem.Disclaimer;
element.ProductOfferDescription = lineItem.ProductOfferDescription; element.ProductImageLink = lineItem.ProductImageLink;
element.IsDiscountCodeRequired = lineItem.IsDiscountCodeRequired; element.DiscountCodes = lineItem.DiscountCode;
switch (lineItem.DiscountMethod) {
case 0: // Percentage Discount
if (lineItem.DiscountPercent == 0) {
element.ProductPromotionStatement = String.Empty; }
else if (lineItem.DiscountPercent == 100) {
element.ProductPromotionStatement = "Get " + element.ProductName
+ " for free";
} else
{
element.ProductPromotionStatement = (typeOfDiscount ==
PeriodicDiscountOfferType.MultipleBuy) ? "Buy atleast " +
Decimal.ToInt32(lineItem.MinimumQuantity) + " and " : String.Empty; element.ProductPromotionStatement += "Get " +
decimal.ToInt32(lineItem.DiscountPercent) + "% Off on all " + lineItem.ProductName + "(s)"; }
break;
case 1: // Discount Amount
element.ProductPromotionStatement = "Get each " + lineItem.ProductName + " for $" + decimal.Round(lineItem.DiscountAmount, 2, MidpointRounding.AwayFromZero) + " less";
break;
case 2: // Offer Price
element.ProductPromotionStatement = (typeOfDiscount ==
PeriodicDiscountOfferType.MultipleBuy) ? "Buy atleast " + lineItem.MinimumQuantity + " " : "Buy ";
element.ProductPromotionStatement = lineItem.ProductName + "(s) for $" + decimal.Round(lineItem.OfferPrice, 2, MidpointRounding.AwayFromZero) + "each";
break;
case 3: // Offer Price Inclusive Of Tax
bool isDealPrice = ((from s in offerCollection where s.DiscountMethod
!= 3 select s).Count() == 0) ? true : false;
element.ProductPromotionStatement = "Get " + (isDealPrice ? lineItem.DiscountName : lineItem.ProductName) +
" for $" + decimal.Round(lineItem.OfferPriceInclusiveOfTax, 2, MidpointRounding.AwayFromZero) + "(Inclusive of Taxes)";
break; } returnObject.Add(element); } return returnObject; } /// <summary>
/// Consolidates and converts the Summarized Promotion Statement to an End-User understandable format.
/// </summary>
/// <param name="offerCollection">Contains set of all Line Items for a particular discount Offer.</param>
/// <param name="typeOfDiscount">Is it MultiBuy/ Mix-n-Match/ Discount/ Price Adjustment.</param>
/// <returns>A collection of summarized promotions.</returns> private static List<ChannelPromotionsSummarizedData>
Pass3GenerateConsolidatedPromotionStatement(IEnumerable<ChannelPromotionsSummarizedData> offerCollection, PeriodicDiscountOfferType typeOfDiscount)
{
Customization
var iterator = offerCollection.ToList(); string appendText = " when you buy a "; string productStatement = String.Empty; bool appendFlag = false;
for (int counter = 0; counter < offerCollection.Count(); counter++) {
if
(string.IsNullOrEmpty(iterator.ElementAt(counter).ProductPromotionStatement)) {
appendFlag = true;
appendText += ((string.Compare(appendText, " when you buy a ", StringComparison.OrdinalIgnoreCase) == 0) ? String.Empty : " and ") +
iterator.ElementAt(counter).ProductName; } productStatement = (string.IsNullOrEmpty(iterator.ElementAt(counter).ProductPromotionStatement)) ? productStatement : iterator.ElementAt(counter).ProductPromotionStatement; }
foreach (var lineItem in offerCollection) {
lineItem.ProductPromotionStatement = appendFlag ? productStatement + appendText : lineItem.ProductPromotionStatement;
returnObject.Add(lineItem); } return returnObject; } } }
You cannot customize a workflow that is included with the CRT. However, you can replace it with your own custom workflow or you can add more workflows. The CRT configuration file determines the workflow to load. If you add a new
workflow with the same name as a default workflow above the default workflow, the new workflow replaces the default workflow.
To change the CRT configuration file, do the following: 1. Open commerceRuntime.config in your solution.
Note: The location of this file varies based on your deployment. If you are customizing the SharePoint starter store, you can locate the file by using the Internet Information Services (IIS) Manager. In IIS Manager, find the starter store under the Sites node. Right-click the public starter store, and then click Explore.
2. Add a line for your new workflow to the list of assemblies, fSee the following example.
<add source="assembly" value="ContosoWorkflow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6598494e9dab8361,
Module Review
Test Your Knowledge
Test your knowledge with the following questions.
1. What are the basic steps to create a new Service and add it to the Commerce Runtime?
2. Which CRT service should be customized to send email to customers based on email templates that are defined in Microsoft Dynamics AX?
( ) PaymentService ( ) LoyaltyService ( ) AddressService ( ) EmailService ( ) SalesOrderService
Customization
Test Your Knowledge Solutions
Module Review and Takeaways
1. What are the basic steps to create a new Service and add it to the Commerce Runtime?
MODEL ANSWER:
To create a new service, you must create three different objects: Request object, Response object and Service object. After you create the service, you must change the Commerce Runtime configuration file.
2. Which CRT service should be customized to send email to customers based on email templates that are defined in Microsoft Dynamics AX?
( ) PaymentService ( ) LoyaltyService ( ) AddressService (√) EmailService ( ) SalesOrderService
MODULE 7: TESTING, DEPLOYMENT AND
VERSIONING OF ECOMMERCE SITES
Module Overview
Microsoft Dynamics AX 2012 R3 includes an online starter store (Ecommerce) that is built on Microsoft SharePoint Server 2012. You can create your own online store by using some elements of the starter store, customizing some elements, and adding your own elements.
This module helps you understand different ways to test the customization and how you can deploy the customization across multiple environments, such as development, test, production, and so on.
Objectives
The objectives are as follows:
Explain testing considerations.