Overview
In this guide, we will explore the different reference implementations available for Salesforce Commerce Cloud, and how they can help you to use the connector and transform your Akeneo Data even further on the Salesforce platform. Reference implementations are suggested solutions that provide a starting point for using Akeneo data in your commerce cloud application and leverage automations to help you save time and resources to achieve your specific business requirements.
These implementations are not available in the installed package so you will need the support of a Salesforce Administrator or developer to implement some of the below.
How to trigger a Webstore Search Index after a successful transfer ?
In order to trigger a wesbtore search index refresh we are providing below an invocable flow action that can be used in any flow to trigger and reindex at the end of each successfull transfer log.
- Create invocable Apex Class using the below code
public with sharing class ProductIndexing {
@InvocableMethod (label='Trigger Product Index' description='triggers the product search index automatically' category='B2B Commerce')
public static List<StoreIndexTriggered> triggerProductIndex(List<String> webstoreIds) {
List<StoreIndexTriggered> triggeredIndexes = new List<StoreIndexTriggered>();
if(webstoreIds != null || !webstoreIds.isEmpty()){
for(String webstoreId : webstoreIds){
ConnectApi.CommerceSearchIndex searchIndex = ConnectApi.CommerceSearchSettings.createCommerceSearchIndex(webstoreId,ConnectApi.CommerceSearchIndexBuildType.INCREMENTAL);
StoreIndexTriggered triggeredIndex = new StoreIndexTriggered();
triggeredIndex.webstoreId = webstoreId;
triggeredIndex.isSuccessfullyTriggered = searchIndex != null &&
searchIndex.indexStatus != null &&
searchIndex.indexStatus != ConnectApi.CommerceSearchIndexStatus.FAILED;
triggeredIndexes.add(triggeredIndex);
}
}
return triggeredIndexes;
}
public class StoreIndexTriggered {
@InvocableVariable public String webstoreId;
@InvocableVariable public Boolean isSuccessfullyTriggered;
}
}
Once the class has been create you can access the action in your flows
2. Navigate to Setup > Flows > New Flow
3. Create a record triggered flow when an Akeneo Transfer Log Record is updated and the Status equals “SUCCESS”
4. Run an Asynchronous path and connect a Get Store ID Element
5. In the Get Store ID GET Element you can put the name of the commerce store you want to reindex. In the below example we are using “Commerce Demo Store”
6. Add an ACTION element in your flow called Product Reindex
7. Select the ID from your get element and pass it to your invocable Apex Action
8. Now you can debug and test your flow using the debug functionality from the flow.
9. If flow runs successfully you can proceed to the Activation.
Once the flow is Active you can trigger an Akeneo Catalog Transfer and see if your Webstore Search Index is triggered once the full product transfer was finalised successfully.
How to map your Akeneo Product Catalog to Webstore Catalog and Product Categories?
The current connector data model matches the Salesforce Commerce Cloud data model of Catalogs and Product Categories. As the custom and standard objects directly match, they can be easily synchronized using an external ID field on the standard objects. This external ID will hold the Salesforce ID of the records that it directly matches.
Before proceeding
The external ID fields must be manually created in the three standard Salesforce objects.
Multiple Salesforce platform features can be leveraged to do the actual record synchronisation. For this reference implementation, we choose to use Apex Triggers because it’s the most flexible option. Flows were also considered, but at this moment they don’t support the upsert operation, which would increase quite significantly the complexity of the implementation.
- Create the three below Apex Triggers
- SyncProductCatalog
- SyncProductCategory
- SyncProductCategoryProduct
SyncProductCatalog Trigger
This trigger synchronizes the data between the custom Akeneo Product Catalog and the standard Product Catalog object.
trigger SyncProductCatalog on akeneoSF__Akeneo_Product_Catalog__c (after insert, after update, after delete) {
if (Trigger.isInsert || Trigger.isUpdate) {
List<ProductCatalog> productCatalogs = new List<ProductCatalog>();
for (akeneoSF__Akeneo_Product_Catalog__c akeneoProductCatalog : Trigger.new) {
ProductCatalog productCatalog = new ProductCatalog();
productCatalog.Name = akeneoProductCatalog.Name;
productCatalog.External_ID__c = akeneoProductCatalog.Id;
productCatalogs.add(productCatalog);
}
if (!productCatalogs.isEmpty()) {
upsert productCatalogs External_ID__c;
}
}
if (Trigger.isDelete) {
Set<String> productCatalogExternalIds = new Set<String>();
for (akeneoSF__Akeneo_Product_Catalog__c akeneoProductCatalog : Trigger.old) {
productCatalogExternalIds.add(akeneoProductCatalog.Id);
}
List<ProductCatalog> productCatalogs = [SELECT Id FROM ProductCatalog WHERE External_ID__c IN :productCatalogExternalIds];
if (!productCatalogs.isEmpty()) {
delete productCatalogs;
}
}
}
SyncProductCategory Trigger
This trigger synchronizes the data between the custom Akeneo Product Category and the standard Product Category object.
trigger SyncProductCategory on akeneoSF__Akeneo_Product_Category__c (after insert, after update, after delete) {
if (Trigger.isInsert || Trigger.isUpdate) {
List<ProductCategory> productCategories = new List<ProductCategory>();
for (akeneoSF__Akeneo_Product_Category__c akeneoProductCategory : Trigger.new) {
ProductCategory productCategoryRecord = new ProductCategory();
productCategoryRecord.Name = akeneoProductCategory.Name;
productCategoryRecord.SortOrder = Integer.valueOf(akeneoProductCategory.akeneoSF__Sort_Order__c);
productCategoryRecord.IsNavigational = true;
productCategoryRecord.External_ID__c = akeneoProductCategory.Id;
productCategoryRecord.Catalog = new ProductCatalog(External_ID__c = akeneoProductCategory.akeneoSF__Product_Catalog__c);
if (akeneoProductCategory.akeneoSF__Parent_Category__c != null) {
productCategoryRecord.ParentCategory = new ProductCategory(External_ID__c = akeneoProductCategory.akeneoSF__Parent_Category__c);
}
productCategories.add(productCategoryRecord);
}
if (!productCategories.isEmpty()) {
upsert productCategories External_ID__c;
}
}
if (Trigger.isDelete) {
Set<String> productCategoryExternalIds = new Set<String>();
for (akeneoSF__Akeneo_Product_Category__c akeneoProductCategory : Trigger.old) {
productCategoryExternalIds.add(akeneoProductCategory.Id);
}
List<ProductCategory> productCategories = [SELECT Id FROM ProductCategory WHERE External_ID__c IN :productCategoryExternalIds];
if (!productCategories.isEmpty()) {
delete productCategories;
}
}
}
SyncProductCategoryProduct Trigger
This trigger synchronizes the data between the custom Akeneo Product Category Product and the standard Product Category Product object.
trigger SyncProductCatalog on akeneoSF__Akeneo_Product_Catalog__c (after insert, after update, after delete) {
if (Trigger.isInsert || Trigger.isUpdate) {
List<ProductCatalog> productCatalogs = new List<ProductCatalog>();
for (akeneoSF__Akeneo_Product_Catalog__c akeneoProductCatalog : Trigger.new) {
ProductCatalog productCatalog = new ProductCatalog();
productCatalog.Name = akeneoProductCatalog.Name;
productCatalog.External_ID__c = akeneoProductCatalog.Id;
productCatalogs.add(productCatalog);
}
if (!productCatalogs.isEmpty()) {
upsert productCatalogs External_ID__c;
}
}
if (Trigger.isDelete) {
Set<String> productCatalogExternalIds = new Set<String>();
for (akeneoSF__Akeneo_Product_Catalog__c akeneoProductCatalog : Trigger.old) {
productCatalogExternalIds.add(akeneoProductCatalog.Id);
}
List<ProductCatalog> productCatalogs = [SELECT Id FROM ProductCatalog WHERE External_ID__c IN :productCatalogExternalIds];
if (!productCatalogs.isEmpty()) {
delete productCatalogs;
}
}
}
2. Once the triggers have been created every time a transfer of the Akeneo Product Catalog happens, the relevant category tree and category assignments will be replicated into your Commerce App > Catalog.
How to Import your Akeneo Product Media Assets into CMS ?
The current version of the connector allows the transfer of the Product Media Assets into a custom object. In order to bring the assets public URLs to CMS content we can use the CSV product importer available in Commerce Cloud.
Once you have successfully transferred to your assets from a catalog.
Export the Akeneo Product Asset object data in a CSV file using Data Loader,Dataloader.io, Salesforce Inspector or any other data export tool.
The CSV file should have the following fields:
- Product SKU
- Asset Type
- Asset URL
To import the Assets into CMS navigate to Commerce App > Product Workspace > Click Import
On the next screen upload the CSV file
How to map your Akeneo Product Variants and Axes to Salesforce Product Variations and Attributes?
To display variations in a B2B Commerce store Salesforce uses a more complex data model. It uses the objects Product Attribute Set and Product Attribute Set Item to store a set of “variant axes”. Each set can be assigned to multiple variant parents (or models in Akeneo) using the object Product Attribute Set Product. To define the variant axis's actual value, the Product Attribute object is used, and in that object, a field for each axis must be created.
Synchronization
The Salesforce data model for variants is quite complex and requires a few data transformations to be done.
First of all, because the variant axes data that is received from Akeno doesn’t include any mapping for Salesforce fields, these fields have to be created manually on the Product Attribute object.
Let’s assume that one of the variant axes is called shirt_size
and that in the Product Attribute, a corresponding field called Shirt_Size__c
was created.
And let’s also assume that this was the data created by the connector:
Product
akeneoSF__Akeneo_UUID__c | Name | Type | ProductClass | akeneoSF__Akeneo_Product_Type__c | akeneoSF__Akeneo_Variant_Axes__c | akeneoSF__Akeneo_Variant_Model__c |
---|---|---|---|---|---|---|
code-001 | Summer T-Shirt | Base | VariationParent | MODEL | - | - |
uuid-002 | Summer T-Shirt - Small | - | Variation | VARIANT | shirt_size | code-001 |
uuid-003 | Summer T-Shirt - Large | - | Variation | VARIANT | shirt_size | code-001 |
Akeneo Variant Axes Detail
akeneoSF__Product__c | akeneoSF__Variant_Attribute_Locale__c | akeneoSF__Variant_Attribute_API__c | akeneoSF__Variant_Attribute_Label__c | akeneoSF__Variant_Attribute_Value_API__c | akeneoSF__Variant_Attribute_Value__c |
---|---|---|---|---|---|
uuid-002 | en_GB | shirt_size | Shirt Size | small | Small |
uuid-002 | fr_FR | shirt_size | Taille De Chemise | small | Petit |
uuid-003 | en_GB | shirt_size | Shirt Size | large | Large |
uuid-003 | fr_FR | shirt_size | Taille De Chemise | large | Grand |
With this data, the Salesforce B2B Commerce data model can be created in the following manner:
Akeneo Attribute to Salesforce Field Mapping
Each Akeneo variant axis must be mapped to a Salesforce field on the Product Attribute object.
Akeneo Attribute | Salesforce Field |
---|---|
shirt_size | Shirt_Size__c |
Product Attribute Set
The same attribute set can be linked to multiple variant parents (Akeneo models) but to make things easier, in this example, each variant parent will have its own attribute set, where its name is the model’s code.
Id | DeveloperName | MasterLabel |
---|---|---|
id-1 | code-001 | code-001 |
Product Attribute Set Item
The attribute set items can be taken from the akeneoSF__Akeneo_Variant_Axes__c
field of the Product. This field contains a comma-separated list of the variant axes. The right field for each variant axis can be taken from the previous mapping table.
The Sequence field controls the order by which the variant axes will appear in the B2B Commerce store.
ProductAttributeSetId | Field | Sequence |
---|---|---|
id-1 | Shirt_Size__c | 1 |
Product Attribute Set Product
This object is used to link an attribute set to a product. The attribute set should only be linked to the variant parent.
ProductAttributeSetId | ProductId |
---|---|
id-1 | code-001 |
Product Attribute
This object contains the actual values for each variant axis. It is linked to both the variant parent and the variant product.
VariantParentId | ProductId | Shirt_Size__c |
---|---|---|
code-001 | uuid-002 | small |
code-001 | uuid-003 | large |
Reference Implementation
Apart from the data model complexity, there are a few other constraints that limit the way that the data can be synchronized.
Product Type
In Salesforce, for a product to be considered as a variant parent its standard Type
field must be set to “Base“. The challenge is that this field is not editable, which means that its value must be set when the product is created for the first time.
To overcome this limitation, as part of the reference implementation, it’s recommended to create a before-insert trigger that sets the product type to "Base" when the akeneoSF__Akeneo_Product_Type__c
is "MODEL". This way all product models in Akeneo will have the correct product type when they are inserted for the first time.
// SyncProductModelType
trigger SyncProductModelType on Product2(before insert) {
for (Product2 product : Trigger.new) {
if (product.akeneoSF__Akeneo_Product_Type__c == 'MODEL') {
product.Type = 'Base';
}
}
}
If the product models already exist in Salesforce before this trigger is deployed, a manual step is required to delete these products. Alternatively, instead of deleting the products, the akeneoSF__Akeneo_UUID__c field can be cleared which will result in new product models to be created by the connector.
Setup and Non-Setup Objects
Salesforce has the concept of setup and non-setup objects. Among other things, one of the most important implications is that DML operations on certain setup objects can’t be mixed with non-setup objects in the same transaction. And because the Product Attribute Set and Product Attribute Set Item objects are considered non-setup objects they can’t be inserted together with the other objects from the variants data model (considered setup objects) in the same transaction.
This limitation causes the reference implementation to be more complex than normal because the DML operations have to split between different transactions. The recommended way to do this is to use the Salesforce Batch Apex feature. This allows the creation of multiple batch jobs, each running in a different transaction, that can process all product records and perform the necessary DMLs on the variants data model.
Therefore, for this reference implementation, three batch jobs were created, each processing different objects.
Batch Job | Objects |
---|---|
SyncProductAttributeSetBatch | Product Attribute Set, Product Attribute Set Item |
SyncProductAttributeSetProductBatch | Product Attribute Set Product |
SyncProductAttributeBatch | Product Attribute |
These batch jobs are calling each other in the correct order, but a different mechanism is required to call the first one and start the process. For that, as part of the reference implementation, a trigger on the Akeneo Transfer Log object was created. This trigger fires when the akeneoSF__CompletionDate__c
field of the transfer log is populated, which means the transfer has finished. It then calls the SyncProductAttributeSetBatch
and passes to it the akeneoSF__StartDate__c
field of the transfer log, which will cause the batch to run only on product records that were updated after the start of the transfer.
// TransferLogTrigger
trigger TransferLogTrigger on akeneoSF__Akeneo_Transfer_Log__c(after update) {
akeneoSF__Akeneo_Transfer_Log__c singleTransferLogNew = Trigger.new[0];
akeneoSF__Akeneo_Transfer_Log__c singleTransferLogOld = Trigger.old[0];
if (singleTransferLogOld.akeneoSF__CompletionDate__c == null && singleTransferLogNew.akeneoSF__CompletionDate__c != null) {
Database.executeBatch(new SyncProductAttributeSetBatch(singleTransferLogOld.akeneoSF__StartDate__c));
}
}
// SyncProductAttributeSetBatch
public class SyncProductAttributeSetBatch implements Database.Batchable<sObject>, Database.Stateful {
private static Map<String, String> akeneoAttributeFieldMap = new Map<String, String>{ 'shirt_size' => 'Shirt_Size__c' };
private Datetime lastModifiedDate;
public Set<Id> productModelIds;
public SyncProductAttributeSetBatch(Datetime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
this.productModelIds = new Set<Id>();
}
public Iterable<Product2> start(Database.BatchableContext bc) {
return [
SELECT Id, akeneoSF__Akeneo_UUID__c, akeneoSF__Akeneo_Variant_Axes__c
FROM Product2
WHERE akeneoSF__Akeneo_Product_Type__c = 'MODEL' AND LastModifiedDate >= :lastModifiedDate
];
}
public void execute(Database.BatchableContext bc, List<Product2> scope) {
Map<String, Id> productModelCodeToIdMap = new Map<String, Id>();
Map<String, Set<String>> productModelIdVariationAxesMap = new Map<String, Set<String>>();
for (Product2 product : scope) {
if (product.akeneoSF__Akeneo_Variant_Axes__c != null) {
productModelCodeToIdMap.put(product.akeneoSF__Akeneo_UUID__c, product.Id);
productModelIdVariationAxesMap.put(product.akeneoSF__Akeneo_UUID__c, new Set<String>(product.akeneoSF__Akeneo_Variant_Axes__c.split(',')));
this.productModelIds.add(product.Id);
}
}
// get existing product attribute sets
Set<String> existingProductAttributeSetModelCodes = new Set<String>();
for (ProductAttributeSet existingProductAttributeSet : [
SELECT Id, MasterLabel
FROM ProductAttributeSet
WHERE MasterLabel IN :productModelCodeToIdMap.keySet()
]) {
existingProductAttributeSetModelCodes.add(existingProductAttributeSet.MasterLabel);
}
// create the product attribute sets
List<ProductAttributeSet> productAttributeSetList = new List<ProductAttributeSet>();
for (String productModelCode : productModelCodeToIdMap.keySet()) {
if (!existingProductAttributeSetModelCodes.contains(productModelCode)) {
productAttributeSetList.add(new ProductAttributeSet(DeveloperName = productModelCode.replace(' ', '_'), MasterLabel = productModelCode));
}
}
if (!productAttributeSetList.isEmpty()) {
insert productAttributeSetList;
}
Map<String, Id> productAttributeSetModelCodeToIdMap = new Map<String, Id>();
for (ProductAttributeSet productAttributeSet : productAttributeSetList) {
productAttributeSetModelCodeToIdMap.put(productAttributeSet.MasterLabel, productAttributeSet.Id);
}
// get field definitions
Set<String> fieldNames = new Set<String>();
for (Set<String> variationAxes : productModelIdVariationAxesMap.values()) {
for (String variationAxis : variationAxes) {
String fieldName = akeneoAttributeFieldMap.get(variationAxis);
if (fieldName != null) {
fieldNames.add(fieldName.removeEnd('__c'));
}
}
}
List<FieldDefinition> variationFieldDefinitionList = [
SELECT DurableId, DeveloperName
FROM FieldDefinition
WHERE EntityDefinition.QualifiedApiName = 'ProductAttribute' AND DeveloperName IN :fieldNames
];
Map<String, String> variationFieldNameToDurableIdMap = new Map<String, String>();
for (FieldDefinition variationFieldDefinition : variationFieldDefinitionList) {
variationFieldNameToDurableIdMap.put(variationFieldDefinition.DeveloperName, variationFieldDefinition.DurableId.split('\\.')[1]);
}
// create the product attribute set items
List<ProductAttributeSetItem> productAttributeSetItemList = new List<ProductAttributeSetItem>();
for (String productModelCode : productModelCodeToIdMap.keySet()) {
if (!existingProductAttributeSetModelCodes.contains(productModelCode)) {
Integer sequence = 0;
for (String variationAxis : productModelIdVariationAxesMap.get(productModelCode)) {
String fieldName = akeneoAttributeFieldMap.get(variationAxis);
Id fieldId = variationFieldNameToDurableIdMap.get(fieldName.removeEnd('__c'));
if (fieldName != null && fieldId != null) {
productAttributeSetItemList.add(
new ProductAttributeSetItem(
ProductAttributeSetId = productAttributeSetModelCodeToIdMap.get(productModelCode),
Field = fieldId,
Sequence = sequence++
)
);
}
}
}
}
if (!productAttributeSetItemList.isEmpty()) {
insert productAttributeSetItemList;
}
}
public void finish(Database.BatchableContext bc) {
if (!this.productModelIds.isEmpty()) {
Database.executeBatch(new SyncProductAttributeSetProductBatch(this.productModelIds));
}
}
}
// SyncProductAttributeSetProductBatch
public class SyncProductAttributeSetProductBatch implements Database.Batchable<sObject>, Database.Stateful {
private Set<Id> productModelIds;
public SyncProductAttributeSetProductBatch(Set<Id> productModelIds) {
this.productModelIds = productModelIds;
}
public Iterable<Product2> start(Database.BatchableContext bc) {
return [
SELECT Id, akeneoSF__Akeneo_UUID__c
FROM Product2
WHERE Id IN :productModelIds
];
}
public void execute(Database.BatchableContext bc, List<Product2> scope) {
// get the product model codes
Set<String> productModelCodes = new Set<String>();
for (Product2 product : scope) {
productModelCodes.add(product.akeneoSF__Akeneo_UUID__c);
}
// get the product model attribute sets
List<ProductAttributeSet> productAttributeSetList = [SELECT Id, MasterLabel FROM ProductAttributeSet WHERE MasterLabel IN :productModelCodes];
Map<String, Id> productModelCodeToAttributeSetIdMap = new Map<String, Id>();
for (ProductAttributeSet productAttributeSet : productAttributeSetList) {
productModelCodeToAttributeSetIdMap.put(productAttributeSet.MasterLabel, productAttributeSet.Id);
}
// get existing product attribute set products
Set<String> existingProductAttributeSetProductKeys = new Set<String>();
for (ProductAttributeSetProduct productAttributeSetProduct : [
SELECT Id, ProductId, ProductAttributeSetId
FROM ProductAttributeSetProduct
WHERE ProductId IN :productModelIds AND ProductAttributeSetId IN :productModelCodeToAttributeSetIdMap.values()
]) {
existingProductAttributeSetProductKeys.add(productAttributeSetProduct.ProductId + '_' + productAttributeSetProduct.ProductAttributeSetId);
}
// assign the product attribute sets to the product models
List<ProductAttributeSetProduct> productAttributeSetProductList = new List<ProductAttributeSetProduct>();
for (Product2 product : scope) {
if (productModelCodeToAttributeSetIdMap.containsKey(product.akeneoSF__Akeneo_UUID__c)) {
Id productAttributeSetId = productModelCodeToAttributeSetIdMap.get(product.akeneoSF__Akeneo_UUID__c);
if (!existingProductAttributeSetProductKeys.contains(product.Id + '_' + productAttributeSetId)) {
productAttributeSetProductList.add(new ProductAttributeSetProduct(ProductId = product.Id, ProductAttributeSetId = productAttributeSetId));
}
}
}
if (!productAttributeSetProductList.isEmpty()) {
insert productAttributeSetProductList;
}
}
public void finish(Database.BatchableContext bc) {
if (!this.productModelIds.isEmpty()) {
Database.executeBatch(new SyncProductAttributeBatch(this.productModelIds));
}
}
}
// SyncProductAttributeBatch
public class SyncProductAttributeBatch implements Database.Batchable<sObject> {
private static Map<String, String> akeneoAttributeFieldMap = new Map<String, String>{ 'shirt_size' => 'Shirt_Size__c' };
private Set<Id> productModelIds;
public SyncProductAttributeBatch(Set<Id> productModelIds) {
this.productModelIds = productModelIds;
}
public Iterable<Product2> start(Database.BatchableContext bc) {
return [
SELECT Id, akeneoSF__Akeneo_UUID__c
FROM Product2
WHERE Id IN :productModelIds
];
}
public void execute(Database.BatchableContext bc, List<Product2> scope) {
// get product ids for the product models in the scope
Set<Id> scopeProductModelIds = new Set<Id>();
for (Product2 productModel : scope) {
scopeProductModelIds.add(productModel.Id);
}
// get configured default locale
akeneoSF__Akeneo_Connector_Configuration__mdt connectorConfigurationMetadata = akeneoSF__Akeneo_Connector_Configuration__mdt.getInstance('Akeneo');
String defaultLocale = connectorConfigurationMetadata.akeneoSF__Default_Locale__c != null
? connectorConfigurationMetadata.akeneoSF__Default_Locale__c
: 'en_US';
// get variation axes
List<akeneoSF__Akeneo_Variant_Axes_Detail__c> variationAxes = [
SELECT
Id,
akeneoSF__Variant_Attribute_API__c,
akeneoSF__Variant_Attribute_Label__c,
akeneoSF__Variant_Attribute_Value_API__c,
akeneoSF__Variant_Attribute_Value__c,
akeneoSF__Variant_Attribute_Locale__c,
akeneoSF__Product__c,
akeneoSF__Product__r.akeneoSF__Akeneo_Variant_Model__c
FROM akeneoSF__Akeneo_Variant_Axes_Detail__c
WHERE akeneoSF__Product__r.akeneoSF__Akeneo_Variant_Model__c IN :scopeProductModelIds AND akeneoSF__Variant_Attribute_Locale__c = :defaultLocale
];
Map<Id, List<akeneoSF__Akeneo_Variant_Axes_Detail__c>> variationAxesByProductVariantId = new Map<Id, List<akeneoSF__Akeneo_Variant_Axes_Detail__c>>();
for (akeneoSF__Akeneo_Variant_Axes_Detail__c variationAxis : variationAxes) {
if (!variationAxesByProductVariantId.containsKey(variationAxis.akeneoSF__Product__c)) {
variationAxesByProductVariantId.put(variationAxis.akeneoSF__Product__c, new List<akeneoSF__Akeneo_Variant_Axes_Detail__c>());
}
variationAxesByProductVariantId.get(variationAxis.akeneoSF__Product__c).add(variationAxis);
}
// get existing product attributes
List<ProductAttribute> existingProductAttributes = [
SELECT Id, ProductId, VariantParentId
FROM ProductAttribute
WHERE VariantParentId IN :scopeProductModelIds AND ProductId IN :variationAxesByProductVariantId.keySet()
];
Set<String> existingProductAttributeKeys = new Set<String>();
for (ProductAttribute existingProductAttribute : existingProductAttributes) {
existingProductAttributeKeys.add(existingProductAttribute.VariantParentId + '_' + existingProductAttribute.ProductId);
}
// create product attributes
List<ProductAttribute> productAttributeList = new List<ProductAttribute>();
for (Id productVariantId : variationAxesByProductVariantId.keySet()) {
List<akeneoSF__Akeneo_Variant_Axes_Detail__c> variationAxesForProductVariant = variationAxesByProductVariantId.get(productVariantId);
if (variationAxesForProductVariant == null || variationAxesForProductVariant.isEmpty()) {
continue;
}
Id productModelId = variationAxesForProductVariant[0].akeneoSF__Product__r.akeneoSF__Akeneo_Variant_Model__c;
if (existingProductAttributeKeys.contains(productModelId + '_' + productVariantId)) {
continue;
}
ProductAttribute productAttribute = new ProductAttribute(ProductId = productVariantId, VariantParentId = productModelId);
for (akeneoSF__Akeneo_Variant_Axes_Detail__c variationAxis : variationAxesForProductVariant) {
String attributeField = akeneoAttributeFieldMap.get(variationAxis.akeneoSF__Variant_Attribute_API__c);
if (attributeField != null) {
productAttribute.put(attributeField, variationAxis.akeneoSF__Variant_Attribute_Value_API__c);
}
}
productAttributeList.add(productAttribute);
}
if (!productAttributeList.isEmpty()) {
insert productAttributeList;
}
}
public void finish(Database.BatchableContext bc) {
}
}