Im ersten Teil dieser Serie ging es darum einen kurzen Überblick über das Sitecore Commerce Connect Modul zu verschaffen. In diesem Beitrag geht es nun um die Synchronisation von Produkten zwischen einem externen Shopsystem (kurz ECS, in unserem PoC Hybris) und Sitecore.
Zunächst ist es wichtig zu verstehen, dass Preise und Bestand im Commerce Connect nicht als Produktstammdaten gelten. Der Bestand ändert sich ständig und DEN Preis gibt es ja auch nicht, Preisfindung ist ein sehr komplexes Thema. Sitecore geht davon aus, dass diese Informationen online vom ECS ermittelt und geliefert werden. Im Teil 4 dieser Serie wird dann beschrieben, wie ihr auch an diese Informationen kommt.
Die Hoheit für die Produktdaten liegt sicherlich im ECS. Für die Synchronisation der Produktdaten schlägt Sitecore folgende Varianten vor:
- Produkte als Items in Sitecore Datenbank (optional bidirektional)
- Produkte in ECS, Zugriff über Sitecore Data Provider
- Produkte in ECS, Zugriff über Sitecore Index
Wir haben uns im Verlauf des PoC für die Variante 1 entschieden, da diese standardmäßig vom Sitecore so vorgesehen ist und den höchsten Komfort bietet. In der Sitecore Commerce Components Overview im Kapitel “Products” sind einige Argumente für oder gegen die unterschiedlichen Variante aufgeführt.
So, jetzt aber Butter bei die Fische 🙂
Installation Modul
Falls ihr das Modul noch nicht installiert habt, findet ihr wie gewohnt eine gute Installationsanleitung auf dev.sitecore.net.
Synchronisation Produkte
Das Ziel: Titel, Beschreibung und Bilder der Produkte aus dem externen Shopsystem mit Sitecore zu synchronisieren.
In folgende Pipelines der Sitecore.Commerce.Products.config muss für die unidirektionale Synchronisation der Produkte eingegriffen werden:
Pipeline getExternalCommerceSystemProductList
.config
[xml]
<!– GET EXTERNAL COMMERCE SYSTEM PRODUCT LIST
This pipeline is responsible for obtaining the list of product Ids to be synchronized from the external commerce system.–>
<commerce.synchronizeProducts.getExternalCommerceSystemProductList>
<processor type="Sitecore.Commerce.Pipelines.Products.GetExternalCommerceSystemProductList.GetExternalCommerceSystemProductList, Sitecore.Commerce">
<patch:delete />
</processor>
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products.ReadProducts, Comspace.Sitecore.CommerceConnect.Hybris" />
</commerce.synchronizeProducts.getExternalCommerceSystemProductList>
[/xml]
Processor ReadProducts
[csharp]
using System.Collections.Generic;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;
namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products
{
/// <summary>
/// commerce.synchronizeProducts.getExternalCommerceSystemProductList
/// </summary>
public class ReadProducts : PipelineProcessor<ServicePipelineArgs>
{
public override void Process(ServicePipelineArgs args)
{
var request = args.Request as SynchronizeProductsRequest;
var productIds = args.Request.Properties["ExternalCommerceSystemProductIds"] as List<string>; //integration guide (page 12)
productIds = productIds ?? new List<string>();
//get products from ECS
IEnumerable<ExternalProduct> externalProducts = ProductConnector.Load(request.Language);
//convert and add to commerce connect list
productIds.AddRange(from product in externalProducts
where product.Code != null
select product.Code);
//persist for next processor
args.Request.Properties["ExternalCommerceSystemProductIds"] = productIds;
}
}
}
[/csharp]
Pipeline synchronizeProductEntity
.config
[xml]
<!– SYNCHRONIZE PRODUCT ITEM –>
<commerce.synchronizeProducts.synchronizeProductEntity>
<processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeProductEntity.ReadProductFromSitecore, Sitecore.Commerce">
<patch:delete />
</processor>
<processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeProductEntity.SaveProductToExternalCommerceSystem, Sitecore.Commerce" >
<patch:delete />
</processor>
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products.ReadProduct, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Products.SynchronizeProductEntity.ReadExternalCommerceSystemProduct, Sitecore.Commerce‘]" />
</commerce.synchronizeProducts.synchronizeProductEntity>
[/xml]
Processor ReadProduct
[csharp]
using Comspace.Sitecore.CommerceConnect.Entities.Products;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;
using Sitecore.Diagnostics;
namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products
{
/// <summary>
/// commerce.synchronizeProducts.synchronizeProductEntity
/// </summary>
public class ReadProduct : PipelineProcessor<ServicePipelineArgs>
{
public override void Process(ServicePipelineArgs args)
{
SynchronizeProductRequest syncProdRequest = args.Request as SynchronizeProductRequest;
//get product from ECS
ExternalProduct externalProduct = ProductConnector.Load(syncProdRequest.Language, syncProdRequest.ProductId); //TBD
if (externalProduct != null && IsProductValidForImport(externalProduct))
{
CustomProduct product = new CustomProduct();
product.ExternalId = externalProduct.Code;
product.Name = externalProduct.Name;
product.FullDescription = externalProduct.Summary;
product.ImageUrl = HybrisSettings.Url + externalProduct.ImageUrl; //custom property
//persist for next processor
args.Request.Properties["Product"] = product; //integration guide (page 12)
}
}
protected bool IsProductValidForImport(ExternalProduct product)
{
var result = true;
if (string.IsNullOrEmpty(product.Name)) // = item.Name
{
result = false;
Log.Info("Skip product ‚" + product.Code + "‘: Name not valid.", product); //NOTE System.Messages
}
return result;
}
}
}
[/csharp]
CustomProduct
Wenn auch die Bilder synchronisiert werden sollen, muss das Produkt-Template des Commerce Connect erweitert werden. Konkret müssen dafür folgende Anpassungen vorgenommen werden:
Template
Neues Template “CustomProduct” anlegen, welches vom Standard Commerce Connect Template „Product“ erbt.
Klassen
Erweiterte Produkt-Klasse „CustomProduct“ definieren:
[csharp]
using Sitecore.Commerce.Entities.Products;
using Sitecore.Data.Items;
namespace Comspace.Sitecore.CommerceConnect.Entities.Products
{
/// <summary>
/// Custom product including image field.
/// </summary>
public class CustomProduct : Product
{
public string ImageUrl { get; set; }
public MediaItem Image { get; set; }
}
}
[/csharp]
Die Klasse „CustomProductRepository“ definiert dann wie das erweiterte Produkt gelesen und geschrieben wird:
[csharp]
using Comspace.Sitecore.CommerceConnect.Entities.Products;
using Comspace.Sitecore.CommerceConnect.Model.sitecore.templates.User_Defined.Comspace.CommerceConnect;
using Sitecore.Commerce.Data.Products;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Product = Sitecore.Commerce.Entities.Products.Product;
namespace Comspace.Sitecore.CommerceConnect.Data.Products
{
/// <summary>
/// Custom product repository including image field.
/// </summary>
public class CustomProductRepository : ProductRepository
{
/// <summary>
/// Save product data to related item.
/// </summary>
/// <param name="entityItem"></param>
/// <param name="product"></param>
protected override void UpdateEntityItem(Item entityItem, Product product)
{
base.UpdateEntityItem(entityItem, product);
using (new EditContext(entityItem))
{
var url = (product as CustomProduct).ImageUrl;
var image = GetMediaItem(url, product); //TBD
ImageField imagefield = entityItem.Fields[IProductConstants.ImageFieldName];
imagefield.Alt = image.Alt;
imagefield.MediaID = image.ID;
}
}
/// <summary>
/// Read product data from related item.
/// </summary>
/// <param name="entityItem"></param>
/// <param name="product"></param>
protected override void PopulateEntity(Item entityItem, Product product)
{
base.PopulateEntity(entityItem, product);
ImageField imageField = entityItem.Fields[IProductConstants.ImageFieldName];
(product as CustomProduct).Image = imageField == null ? null : imageField.MediaItem;
}
#region Handle MediaItem
//…
#endregion
}
}
[/csharp]
.config
Angepasste Templates und Klassen in Sitecore.Commerce.Products.config registrieren:
[xml]
<!– PRODUCT REPOSITORY –>
<productRepository type="Sitecore.Commerce.Data.Products.ProductRepository, Sitecore.Commerce">
<patch:delete />
</productRepository>
<productRepository type="Comspace.Sitecore.CommerceConnect.Data.Products.CustomProductRepository, Comspace.Sitecore.CommerceConnect" singleInstance="true">
<template>{0C589D66-A119-435A-907F-43481CD5199F}</template>
<branch>{0C589D66-A119-435A-907F-43481CD5199F}</branch>
<path ref="paths/products" />
<Prefix>Product_</Prefix>
<ProductsIndex>commerce_products_master_index</ProductsIndex>
<ManufacturerRepository ref="productManufacturerRepository" />
<DivisionRepository ref="productDivisionRepository" />
<TypeRepository ref="productTypeRepository" />
<ClassificationsRepository ref="productClassificationsFieldRepository" />
<ResourcesRepository ref="productResourcesRepository" />
<RelationsRepository ref="productRelationsRepository" />
<GlobalSpecificationsRepository ref="productGlobalSpecificationsRepository" />
<ClassificationsSpecificationsRepository ref="productClassificationsSpecificationsRepository" />
<TypeSpecificationsRepository ref="productTypeSpecificationsRepository" />
</productRepository>
<includeTemplates>
<ProductTemplateId>{0C589D66-A119-435A-907F-43481CD5199F}</ProductTemplateId>
</includeTemplates>
<!– Commerce ENTITIES
Contains all the Commerce cart entities.
The configuration can be used to substitute the default entity implementation with extended one. –>
<commerce.Entities>
<Product type="Sitecore.Commerce.Entities.Products.Product, Sitecore.Commerce" >
<patch:delete />
</Product>
<Product type="Comspace.Sitecore.CommerceConnect.Entities.Products.CustomProduct, Comspace.Sitecore.CommerceConnect" />
</commerce.Entities>
[/xml]
Stolpersteine
In der Klasse „Sitecore.Commerce.Templates“ existiert eine Konstante „ProductTemplateId“, deren Id fest verdrahtet ist und nicht auf die zuvor eingerichteten Konfigurationsdateien verweist. Die Konstante wird u.a. in der Klasse „ItemClassificationService“ verwendet, daher ist dieser auszutauschen:
[xml]
<itemClassificationService type="Sitecore.Commerce.Products.ItemClassificationService, Sitecore.Commerce">
<patch:delete />
</itemClassificationService>
<itemClassificationService type="Comspace.Sitecore.CommerceConnect.Entities.ItemClassificationService, Comspace.Sitecore.CommerceConnect" />
[/xml]
Resumé
Zuletzt möchte ich ein paar erste Eindrücke und Erkenntnisse zusammenfassen:
- Der erste Einstieg durch Pipelines in Pipelines in Pipelines ist herausfordernd.
- Es gibt umfangreiche Dokumentationen.
- Einige hilfreiche Klassen aus Beispielen im Internet verweisen leider auf die CommerceServer API, z.B. ProductsSearchResult und CommerceConstants. Auch in dem Dynamics AX Demoshop von Sitecore (Commerce.Dynamics.Storefront) sind Verweise auf die CommerceServer API enthalten. Die Trennung bzw. Abstraktion ist m.E. noch nicht ganz sauber erfolgt.
- Verwunderlich fand ich zunächst, dass die Synchronisation von Bildern nicht im Standard enthalten ist. Aber dem von Sitecore definierten Prinzip “kleinster gemeinsamer Nenner” folgend, ist es verständlich, denn Bild-Quelle und -Ziel sind doch sehr projektspezifisch.
- Der “Merchandising Manager” ist NICHT Bestandteil des Commerce Connect, sondern des CommerceServer Connect.
- In einem echten Projekt mit einem umfangreichen Datenmodell wird der größte Aufwand in das Verständnis und das Mapping der Datenmodelle gehen.
Wenn ihr mehr zum Sitecore Commerce Connect wissen wollt, dann schaut mal hier: