Synchronize OrderCloud Environments Using Endpoints
In the previous blog post, we discussed how you can seed a new environment using the Headstart middleware application. In this one, we are going to use that seeded environment and synchronize it with content from another OC environment which already have categories, products and suppliers. For this, we will use the OC middleware project from the OC Headstart repository as a base by adding a new endpoint to fulfill this request. So without further due, lets begin.
First, we will create a model for the request which will contain the client id and secret for the environment that we are going to copy the content, the client id, secret and marketplace id for the destination environment.
namespace Headstart.Common.Services.Portal.Models
{
public class SyncMarketplaces
{
public string FromClientId { get; set; }
public string FromClientSecret { get; set; }
public string ToClientId { get; set; }
public string ToClientSecret { get; set; }
public string ToMarketplaceId { get; set; }
}
}
Then, we will create a sync controller which will call a sync command class to execute the synchronization. Pay special attention to the OrderCloud User Auth attribute which will restrict the access of the endpoint to users with Product Admin or Catalog Admin roles.
using Headstart.API.Commands.Crud;
using Headstart.Common.Services.Portal.Models;
using Microsoft.AspNetCore.Mvc;
using OrderCloud.Catalyst;
using OrderCloud.SDK;
using System.Threading.Tasks;
namespace Headstart.Common.Controllers
{
[Route("sync")]
public class SyncController : CatalystController
{
private readonly ISyncCommand _command;
public SyncController(ISyncCommand command)
{
_command = command;
}
/// <summary>
/// Syncronize between two environments
/// </summary>
[HttpPost, Route("from-env-all"), OrderCloudUserAuth(ApiRole.ProductAdmin, ApiRole.CatalogAdmin)]
public async Task<string> FromEnvAll([FromBody] SyncMarketplaces syncMarketplaces)
{
return await _command.FromEnvAll(syncMarketplaces);
}
}
}
Now, we will create the sync command class which will implement an interface so it can be instantiated using dependency injection. The code for this class is quite extensive so we will cover it step by step and the full code for this class is available in the file below
First, we have the interface which only has one method FromEnvAll which receives as parameter the sync marketplaces data
public interface ISyncCommand
{
Task<string> FromEnvAll(SyncMarketplaces syncMarketplaces);
}
Then, we have the class itself and the constructor which instantiate the order cloud client, app settings and portal service
public class SyncCommand : ISyncCommand
{
private readonly AppSettings _settings;
private readonly IPortalService _portalService;
private readonly IOrderCloudClient _oc;
public SyncCommand(AppSettings settings, IOrderCloudClient elevatedOc, IPortalService portalService)
{
_oc = elevatedOc;
_settings = settings;
_portalService = portalService;
}
}
We then have the FromEnvAll method implementation which creates two order cloud clients, one for the origin marketplace and one for the destination marketplace, we then get all the products from the origin marketplace and call the ProcessSellerData and ProcessSupplierData functions with the corresponding data.
public async Task<string> FromEnvAll(SyncMarketplaces syncMarketplaces)
{
try
{
var oc = new OrderCloudClient(new OrderCloudClientConfig
{
ClientId = syncMarketplaces.ToClientId,
ClientSecret = syncMarketplaces.ToClientSecret,
ApiUrl = _settings.OrderCloudSettings.ApiUrl,
AuthUrl = _settings.OrderCloudSettings.ApiUrl,
Roles = new[] { ApiRole.FullAccess }
});
var ocFromMarketplace = new OrderCloudClient(new OrderCloudClientConfig
{
ClientId = syncMarketplaces.FromClientId,
ClientSecret = syncMarketplaces.FromClientSecret,
ApiUrl = _settings.OrderCloudSettings.ApiUrl,
AuthUrl = _settings.OrderCloudSettings.ApiUrl,
Roles = new[] { ApiRole.FullAccess }
});
// Get all products
var allProducts = await ocFromMarketplace.Products.ListAllAsync<HSProduct>();
// Seller process
await ProcessSellerData(oc, ocFromMarketplace, allProducts, syncMarketplaces.ToMarketplaceId);
// Supplier process
await ProcessSupplierData(oc, ocFromMarketplace, allProducts, syncMarketplaces.ToMarketplaceId);
return "OK";
}
catch (Exception ex)
{
return ex.Message;
}
}
The process seller data method will start copying the admin addresses, then the admin users, catalogs, categories and then products that belong only to the seller, including price schedule, specs, variants and catalog assignments.
private async Task<string> ProcessSellerData(OrderCloudClient oc, OrderCloudClient ocFromMarketplace, List<HSProduct> allProducts, string marketplaceId)
{
// Get all seller addresses
var addresses = await ocFromMarketplace.AdminAddresses.ListAllAsync();
foreach (var address in addresses)
{
try
{
var oldAddress = await oc.AdminAddresses.GetAsync(address.ID);
if (oldAddress == null)
{
await oc.AdminAddresses.CreateAsync(address);
}
else
{
await oc.AdminAddresses.SaveAsync(oldAddress.ID, address);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await oc.AdminAddresses.CreateAsync(address);
}
else
{
throw;
}
}
}
// Get all seller users
var users = await ocFromMarketplace.AdminUsers.ListAllAsync();
foreach (var user in users)
{
try
{
var oldUser = await oc.AdminUsers.GetAsync(user.ID);
if (oldUser == null)
{
await oc.AdminUsers.CreateAsync(user);
}
else
{
await oc.AdminUsers.SaveAsync(oldUser.ID, user);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
try
{
var oldUsers = await oc.AdminUsers.ListAllAsync();
var oldUserWithSameUserName = oldUsers.SingleOrDefault(x => x.Username == user.Username);
if (oldUserWithSameUserName == null)
{
await oc.AdminUsers.CreateAsync(user);
}
else
{
await oc.AdminUsers.SaveAsync(oldUserWithSameUserName.ID, user);
}
}
catch (Exception ex)
{
if (ex.Message.Contains("User.UsernameMustBeUnique"))
{
continue;
}
throw;
}
}
else
{
throw;
}
}
}
// Get all catalogs
var catalogs = await ocFromMarketplace.Catalogs.ListAllAsync();
foreach (var catalog in catalogs)
{
try
{
catalog.OwnerID = marketplaceId;
var oldCatalog = await oc.Catalogs.GetAsync(catalog.ID);
if (catalog == null)
{
await oc.Catalogs.CreateAsync(catalog);
}
else
{
await oc.Catalogs.SaveAsync(oldCatalog.ID, catalog);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await oc.Catalogs.CreateAsync(catalog);
}
else
{
throw;
}
}
// Get all categories
var categories = await ocFromMarketplace.Categories.ListAllAsync(catalog.ID);
foreach (var category in categories)
{
try
{
var oldCategory = await oc.Categories.GetAsync(catalog.ID, category.ID);
if (oldCategory == null)
{
await oc.Categories.CreateAsync(catalog.ID, category);
}
else
{
await oc.Categories.SaveAsync(catalog.ID, oldCategory.ID, category);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await oc.Categories.CreateAsync(catalog.ID, category);
}
else
{
throw;
}
}
}
}
// Get all seller products
var sellerProducts = allProducts.Where(x => x.DefaultSupplierID == null);
foreach (var sellerProduct in sellerProducts)
{
sellerProduct.OwnerID = marketplaceId;
var priceSchedule = await ocFromMarketplace.PriceSchedules.GetAsync(sellerProduct.DefaultPriceScheduleID);
priceSchedule.OwnerID = marketplaceId;
var specs = await ocFromMarketplace.Products.ListAllSpecsAsync(sellerProduct.ID);
specs.ForEach(x => x.OwnerID = marketplaceId);
var variants = await ocFromMarketplace.Products.ListAllVariantsAsync<HSVariant>(sellerProduct.ID);
var hsProduct = new SuperHSProduct()
{
ID = sellerProduct.ID,
Product = sellerProduct,
PriceSchedule = priceSchedule,
Specs = specs,
Variants = variants
};
try
{
var oldSellerProduct = await oc.Products.GetAsync(sellerProduct.ID);
if (oldSellerProduct == null)
{
await Post(hsProduct, oc, false, null);
}
else
{
await Put(oldSellerProduct.ID, hsProduct, oc, null);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await Post(hsProduct, oc, false, null);
}
else
{
throw;
}
}
// Get all products assignments to catalogs and categories
foreach (var catalog in catalogs)
{
var productAssignments = await ocFromMarketplace.Catalogs.ListAllProductAssignmentsAsync(catalog.ID, sellerProduct.ID);
foreach (var productAssigment in productAssignments)
{
await oc.Catalogs.SaveProductAssignmentAsync(productAssigment);
}
var categoryAssignments = await ocFromMarketplace.Categories.ListAllProductAssignmentsAsync(catalogID: catalog.ID, productID: sellerProduct.ID);
foreach (var categoryAssignment in categoryAssignments)
{
await oc.Categories.SaveProductAssignmentAsync(catalog.ID, categoryAssignment);
}
}
}
return "OK";
}
Each copying section will try to find if the entity already exists in the destination environment. If not, it will create a new entity and if it does, it will update the entity.
We also copied some methods that are available in the HSProductCommand class but with some small modifications that take into account that we are using the different OrderCloud clients.
Method | Description | Modified |
private async Task Post(SuperHSProduct superProduct, OrderCloudClient oc, bool fromSupplier, string token, string supplierName = “”) | Create a new product | Yes |
private async Task Put(string id, SuperHSProduct superProduct, OrderCloudClient oc, string token) | Update an existing product | Yes |
private async Task ValidateVariantsAsync(SuperHSProduct superProduct, OrderCloudClient oc, string token) | Validate if the variants of the product are correctly set | No |
private bool IsDifferentVariantWithSameName(Variant variant, Variant currVariant) | Check if there are variants with the same name | No |
private async Task GetSupplierNameForXpFacet(string supplierID, OrderCloudClient oc, string accessToken) | Get supplier name from xp facet | No |
private async void HandleSpecOptionChanges(IList requestSpecs, IList existingSpecs, OrderCloudClient oc, string token) | Check which specs options have changed | No |
private IList ChangedSpecOptions(List requestOptions, List existingOptions) | Returns the list of spec options changed | No |
private bool OptionHasChanges(SpecOption requestOption, List currentOptions) | Check if an spec option has changes | No |
private void ValidateRequestVariant(Variant variant) | Check the variant request is correct | No |
private async Task UpdateRelatedPriceSchedules(PriceSchedule updated, OrderCloudClient oc, string token) | Update the price schedule of the product | No |
private bool HasVariantChange(Variant variant, Variant currVariant) | Check a variant has changes | No |
The process supplier data method will start getting all the suppliers in the origin environment and then for each supplier will create/update the supplier, then it will try to copy the supplier addresses, supplier users, then products that belong only to the current supplier, including price schedule, specs, variants and catalog assignments and finally any promotion found in the origin environment.
private async Task<string> ProcessSupplierData(OrderCloudClient oc, OrderCloudClient ocFromMarketplace, List<HSProduct> allProducts, string marketplaceId)
{
// Get all suppliers
var suppliers = await ocFromMarketplace.Suppliers.ListAllAsync();
foreach (var supplier in suppliers)
{
try
{
var oldSupplier = await oc.Suppliers.GetAsync(supplier.ID);
if (oldSupplier == null)
{
await oc.Suppliers.CreateAsync(supplier);
}
else
{
await oc.Suppliers.SaveAsync(oldSupplier.ID, supplier);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await oc.Suppliers.CreateAsync(supplier);
}
else
{
throw;
}
}
// Get all supplier addresses
var suppliersAddresses = await ocFromMarketplace.SupplierAddresses.ListAllAsync(supplier.ID);
foreach (var supplierAddress in suppliersAddresses)
{
try
{
var oldSupplierAddress = await oc.SupplierAddresses.GetAsync(supplier.ID, supplierAddress.ID);
if (oldSupplierAddress == null)
{
await oc.SupplierAddresses.CreateAsync(supplier.ID, supplierAddress);
}
else
{
await oc.SupplierAddresses.SaveAsync(supplier.ID, oldSupplierAddress.ID, supplierAddress);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await oc.SupplierAddresses.CreateAsync(supplier.ID, supplierAddress);
}
else
{
throw;
}
}
}
// Get all supplier users
var suppliersUsers = await ocFromMarketplace.SupplierUsers.ListAllAsync(supplier.ID);
foreach (var supplierUser in suppliersUsers)
{
try
{
var oldSupplierUser = await oc.SupplierUsers.GetAsync(supplier.ID, supplierUser.ID);
if (oldSupplierUser == null)
{
await oc.SupplierUsers.CreateAsync(supplier.ID, supplierUser);
}
else
{
await oc.SupplierUsers.SaveAsync(supplier.ID, oldSupplierUser.ID, supplierUser);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
try
{
var oldSupplierUsers = await oc.SupplierUsers.ListAllAsync(supplier.ID);
var oldSupplierUserWithSameUserName = oldSupplierUsers.SingleOrDefault(x => x.Username == supplierUser.Username);
if (oldSupplierUserWithSameUserName == null)
{
await oc.SupplierUsers.CreateAsync(supplier.ID, supplierUser);
}
else
{
await oc.SupplierUsers.SaveAsync(supplier.ID, oldSupplierUserWithSameUserName.ID, supplierUser);
}
}
catch (Exception ex)
{
if (ex.Message.Contains("User.UsernameMustBeUnique"))
{
continue;
}
throw;
}
}
else
{
throw;
}
}
}
// Get all supplier products, THIS WILL PROBABLY NOT WORK
//var userToSaveProducts = suppliersUsers.FirstOrDefault(x => x.AvailableRoles.Contains(nameof(ApiRole.SupplierAdmin)));
string token = null; // await _portalService.Login(userToSaveProducts.Username, userToSaveProducts.Password);
var supplierProducts = allProducts.Where(x => x.DefaultSupplierID == supplier.ID);
foreach (var supplierProduct in supplierProducts)
{
var priceSchedule = await ocFromMarketplace.PriceSchedules.GetAsync(supplierProduct.DefaultPriceScheduleID);
var specs = await ocFromMarketplace.Products.ListAllSpecsAsync(supplierProduct.ID);
var variants = await ocFromMarketplace.Products.ListAllVariantsAsync<HSVariant>(supplierProduct.ID);
var hsProduct = new SuperHSProduct()
{
ID = supplierProduct.ID,
Product = supplierProduct,
PriceSchedule = priceSchedule,
Specs = specs,
Variants = variants
};
try
{
var oldSupplierProduct = await oc.Products.GetAsync(supplierProduct.ID);
if (oldSupplierProduct == null)
{
await Post(hsProduct, oc, true, token, supplier.Name);
}
else
{
await Put(oldSupplierProduct.ID, hsProduct, oc, token);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await Post(hsProduct, oc, true, token, supplier.Name);
}
else
{
throw;
}
}
// Get all products assignments to catalogs and categories
var catalogs = await ocFromMarketplace.Catalogs.ListAllAsync();
foreach (var catalog in catalogs)
{
var productAssignments = await ocFromMarketplace.Catalogs.ListAllProductAssignmentsAsync(catalog.ID, supplierProduct.ID);
foreach (var productAssigment in productAssignments)
{
await oc.Catalogs.SaveProductAssignmentAsync(productAssigment);
}
var categoryAssignments = await ocFromMarketplace.Categories.ListAllProductAssignmentsAsync(catalogID: catalog.ID, productID: supplierProduct.ID);
foreach (var categoryAssignment in categoryAssignments)
{
await oc.Categories.SaveProductAssignmentAsync(catalog.ID, categoryAssignment);
}
}
}
}
// Get all promotions
var promotions = await ocFromMarketplace.Promotions.ListAllAsync();
foreach (var promotion in promotions)
{
try
{
promotion.OwnerID = marketplaceId;
var oldPromotion = await oc.Promotions.GetAsync(promotion.ID);
if (oldPromotion == null)
{
await oc.Promotions.CreateAsync(promotion);
}
else
{
await oc.Promotions.SaveAsync(oldPromotion.ID, promotion);
}
}
catch (OrderCloudException e)
{
if (e.Message.Contains("NotFound:"))
{
await oc.Promotions.CreateAsync(promotion);
}
else
{
throw;
}
}
}
return "OK";
}
Notes:
- This code does not copy information about buyers, buyers users, buyer catalogs and buyer products.
- The code requires that the destination environment has only the seed information to avoid problems.
And that is it. You can now copy the content from one OrderCloud environment to another by calling this endpoint using the swagger UI or any REST client. If you have any questions or suggestions please let me know in the comments. I hope it can help someone and as always keep learning !!!
Leave a Reply