using Headstart.Common; using Headstart.Common.Services; using Headstart.Common.Services.Portal.Models; using Headstart.Models; using ordercloud.integrations.library.Cosmos; using OrderCloud.Catalyst; using OrderCloud.SDK; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; namespace Headstart.API.Commands.Crud { public interface ISyncCommand { Task FromEnvAll(SyncMarketplaces syncMarketplaces); } 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; } public async Task 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(); // 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; } } private async Task ProcessSellerData(OrderCloudClient oc, OrderCloudClient ocFromMarketplace, List 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(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"; } private async Task ProcessSupplierData(OrderCloudClient oc, OrderCloudClient ocFromMarketplace, List 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(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"; } #region Product components private async Task Post(SuperHSProduct superProduct, OrderCloudClient oc, bool fromSupplier, string token, string supplierName = "") { // Determine ID up front so price schedule ID can match superProduct.Product.ID = superProduct.Product.ID ?? CosmosInteropID.New(); await ValidateVariantsAsync(superProduct, oc, token); // Create Specs var defaultSpecOptions = new List(); var specRequests = await Throttler.RunAsync(superProduct.Specs, 100, 5, s => { defaultSpecOptions.Add(new DefaultOptionSpecAssignment { SpecID = s.ID, OptionID = s.DefaultOptionID }); s.DefaultOptionID = null; return oc.Specs.SaveAsync(s.ID, s, accessToken: token); }); // Create Spec Options foreach (Spec spec in superProduct.Specs) { await Throttler.RunAsync(spec.Options, 100, 5, o => oc.Specs.SaveOptionAsync(spec.ID, o.ID, o, accessToken: token)); } // Patch Specs with requested DefaultOptionID await Throttler.RunAsync(defaultSpecOptions, 100, 10, a => oc.Specs.PatchAsync(a.SpecID, new PartialSpec { DefaultOptionID = a.OptionID }, accessToken: token)); // Create Price Schedule PriceSchedule _priceSchedule = null; //All products must have a price schedule for orders to be submitted. The front end provides a default Price of $0 for quote products that don't have one. superProduct.PriceSchedule.ID = superProduct.Product.ID; try { _priceSchedule = await oc.PriceSchedules.CreateAsync(superProduct.PriceSchedule, token); } catch (OrderCloudException ex) { if (ex.HttpStatus == HttpStatusCode.Conflict) { throw new Exception($"Product SKU {superProduct.PriceSchedule.ID} already exists. Please try a different SKU."); } } superProduct.Product.DefaultPriceScheduleID = _priceSchedule.ID; // Create Product if (fromSupplier && !superProduct.Product.xp.Facets.Any()) { superProduct.Product.xp.Facets.Add("supplier", new List() { supplierName }); } var _product = await oc.Products.CreateAsync(superProduct.Product, token); // New: Make Category Product Assignments if (!string.IsNullOrEmpty(superProduct.Product.xp.CatalogId) && !string.IsNullOrEmpty(superProduct.Product.xp.CategoryId)) { await oc.Categories.SaveProductAssignmentAsync(superProduct.Product.xp.CatalogId, new CategoryProductAssignment { ProductID = _product.ID, CategoryID = superProduct.Product.xp.CategoryId }); } // Make Spec Product Assignments await Throttler.RunAsync(superProduct.Specs, 100, 5, s => oc.Specs.SaveProductAssignmentAsync(new SpecProductAssignment { ProductID = _product.ID, SpecID = s.ID }, accessToken: token)); // Generate Variants await oc.Products.GenerateVariantsAsync(_product.ID, accessToken: token); // Patch Variants with the User Specified ID(SKU) AND necessary display xp values await Throttler.RunAsync(superProduct.Variants, 100, 5, v => { var oldVariantID = v.ID; v.ID = v.xp.NewID ?? v.ID; v.Name = v.xp.NewID ?? v.ID; if ((superProduct?.Product?.Inventory?.VariantLevelTracking) == true && v.Inventory == null) { v.Inventory = new PartialVariantInventory { QuantityAvailable = 0 }; } if (superProduct.Product?.Inventory == null) { //If Inventory doesn't exist on the product, don't patch variants with inventory either. return oc.Products.PatchVariantAsync(_product.ID, oldVariantID, new PartialVariant { ID = v.ID, Name = v.Name, xp = v.xp }, accessToken: token); } else { return oc.Products.PatchVariantAsync(_product.ID, oldVariantID, new PartialVariant { ID = v.ID, Name = v.Name, xp = v.xp, Inventory = v.Inventory }, accessToken: token); } }); // List Variants var _variants = await oc.Products.ListVariantsAsync(_product.ID, accessToken: token); // List Product Specs var _specs = await oc.Products.ListSpecsAsync(_product.ID, accessToken: token); // Final result var finalResult = new SuperHSProduct { Product = _product, PriceSchedule = _priceSchedule, Specs = _specs.Items, Variants = _variants.Items, }; // Return the SuperProduct return finalResult; } private async Task Put(string id, SuperHSProduct superProduct, OrderCloudClient oc, string token) { // Update the Product itself var _updatedProduct = await oc.Products.SaveAsync(superProduct.Product.ID, superProduct.Product, token); // Try to fix issue not updating product properly if (_settings.OrderCloudSettings.EnableTimeout) { await Task.Delay(_settings.OrderCloudSettings.DelayTimeoutInSeconds * 1000); } // Two spec lists to compare (requestSpecs and existingSpecs) IList requestSpecs = superProduct.Specs.ToList(); IList existingSpecs = (await oc.Products.ListSpecsAsync(id, accessToken: token))?.Items?.ToList() ?? new List(); // Two variant lists to compare (requestVariants and existingVariants) IList requestVariants = superProduct.Variants; IList existingVariants = (await oc.Products.ListVariantsAsync(id, pageSize: 100, accessToken: token))?.Items?.ToList() ?? new List(); // Calculate differences in specs - specs to add, and specs to delete var specsToAdd = requestSpecs.Where(s => !existingSpecs.Any(s2 => s2.ID == s.ID)).ToList(); var specsToDelete = existingSpecs.Where(s => !requestSpecs.Any(s2 => s2.ID == s.ID)).ToList(); // Get spec options to add -- WAIT ON THESE, RUN PARALLEL THE ADD AND DELETE SPEC REQUESTS foreach (var rSpec in requestSpecs) { foreach (var eSpec in existingSpecs) { if (rSpec.ID == eSpec.ID) { await Throttler.RunAsync(rSpec.Options.Where(rso => !eSpec.Options.Any(eso => eso.ID == rso.ID)), 100, 5, o => oc.Specs.CreateOptionAsync(rSpec.ID, o, accessToken: token)); await Throttler.RunAsync(eSpec.Options.Where(eso => !rSpec.Options.Any(rso => rso.ID == eso.ID)), 100, 5, o => oc.Specs.DeleteOptionAsync(rSpec.ID, o.ID, accessToken: token)); } }; }; // New: Make Category Product Assignments if (!string.IsNullOrEmpty(superProduct.Product.xp.CatalogId) && !string.IsNullOrEmpty(superProduct.Product.xp.CategoryId)) { var currentCategories = await oc.Categories.ListProductAssignmentsAsync(superProduct.Product.xp.CatalogId, productID: _updatedProduct.ID); foreach (var currentCategory in currentCategories.Items) { await oc.Categories.DeleteProductAssignmentAsync(superProduct.Product.xp.CatalogId, superProduct.Product.xp.CategoryId, _updatedProduct.ID); } await oc.Categories.SaveProductAssignmentAsync(superProduct.Product.xp.CatalogId, new CategoryProductAssignment { ProductID = _updatedProduct.ID, CategoryID = superProduct.Product.xp.CategoryId }); } // Create new specs and Delete removed specs var defaultSpecOptions = new List(); await Throttler.RunAsync(specsToAdd, 100, 5, s => { defaultSpecOptions.Add(new DefaultOptionSpecAssignment { SpecID = s.ID, OptionID = s.DefaultOptionID }); s.DefaultOptionID = null; return oc.Specs.SaveAsync(s.ID, s, accessToken: token); }); await Throttler.RunAsync(specsToDelete, 100, 5, s => oc.Specs.DeleteAsync(s.ID, accessToken: token)); // Add spec options for new specs foreach (var spec in specsToAdd) { await Throttler.RunAsync(spec.Options, 100, 5, o => oc.Specs.CreateOptionAsync(spec.ID, o, accessToken: token)); } // Patch Specs with requested DefaultOptionID await Throttler.RunAsync(defaultSpecOptions, 100, 10, a => oc.Specs.PatchAsync(a.SpecID, new PartialSpec { DefaultOptionID = a.OptionID }, accessToken: token)); // Make assignments for the new specs await Throttler.RunAsync(specsToAdd, 100, 5, s => oc.Specs.SaveProductAssignmentAsync(new SpecProductAssignment { ProductID = id, SpecID = s.ID }, accessToken: token)); HandleSpecOptionChanges(requestSpecs, existingSpecs, oc, token); // Check if Variants differ var variantsAdded = requestVariants.Any(v => !existingVariants.Any(v2 => v2.ID == v.ID)); var variantsRemoved = existingVariants.Any(v => !requestVariants.Any(v2 => v2.ID == v.ID)); bool hasVariantChange = false; foreach (Variant variant in requestVariants) { ValidateRequestVariant(variant); var currVariant = existingVariants.Where(v => v.ID == variant.ID); if (currVariant == null || currVariant.Count() < 1) { continue; } hasVariantChange = HasVariantChange(variant, currVariant.First()); if (hasVariantChange) { break; } } // IF variants differ, then re-generate variants and re-patch IDs to match the user input. if (variantsAdded || variantsRemoved || hasVariantChange || requestVariants.Any(v => v.xp.NewID != null)) { //validate variant names before continuing saving. await ValidateVariantsAsync(superProduct, oc, token); // Re-generate Variants await oc.Products.GenerateVariantsAsync(id, overwriteExisting: true, accessToken: token); // Patch NEW variants with the User Specified ID (Name,ID), and correct xp values (SKU) await Throttler.RunAsync(superProduct.Variants, 100, 5, v => { v.ID = v.xp.NewID ?? v.ID; v.Name = v.xp.NewID ?? v.ID; if ((superProduct?.Product?.Inventory?.VariantLevelTracking) == true && v.Inventory == null) { v.Inventory = new PartialVariantInventory { QuantityAvailable = 0 }; } if (superProduct.Product?.Inventory == null) { //If Inventory doesn't exist on the product, don't patch variants with inventory either. return oc.Products.PatchVariantAsync(id, $"{superProduct.Product.ID}-{v.xp.SpecCombo}", new PartialVariant { ID = v.ID, Name = v.Name, xp = v.xp, Active = v.Active }, accessToken: token); } else { return oc.Products.PatchVariantAsync(id, $"{superProduct.Product.ID}-{v.xp.SpecCombo}", new PartialVariant { ID = v.ID, Name = v.Name, xp = v.xp, Active = v.Active, Inventory = v.Inventory }, accessToken: token); } }); }; // If applicable, update OR create the Product PriceSchedule var tasks = new List(); Task _priceScheduleReq = null; if (superProduct.PriceSchedule != null) { _priceScheduleReq = UpdateRelatedPriceSchedules(superProduct.PriceSchedule, oc, token); tasks.Add(_priceScheduleReq); } // List Variants var _variantsReq = oc.Products.ListVariantsAsync(id, pageSize: 100, accessToken: token); tasks.Add(_variantsReq); // List Product Specs var _specsReq = oc.Products.ListSpecsAsync(id, accessToken: token); tasks.Add(_specsReq); await Task.WhenAll(tasks); // Final result var finalResult = new SuperHSProduct { Product = _updatedProduct, PriceSchedule = _priceScheduleReq?.Result, Specs = _specsReq?.Result?.Items, Variants = _variantsReq?.Result?.Items, }; return finalResult; } private async Task ValidateVariantsAsync(SuperHSProduct superProduct, OrderCloudClient oc, string token) { List allVariants = new List(); if (superProduct.Variants == null || !superProduct.Variants.Any()) { return; } try { var allProducts = await oc.Products.ListAllAsync(accessToken: token); if (allProducts == null || !allProducts.Any()) { return; } foreach (Product product in allProducts) { if (product.VariantCount > 0 && product.ID != superProduct.Product.ID) { allVariants.AddRange((await oc.Products.ListVariantsAsync(productID: product.ID, pageSize: 100, accessToken: token)).Items); } } } catch (Exception ex) { return; } foreach (Variant variant in superProduct.Variants) { if (!allVariants.Any()) { return; } List duplicateSpecNames = allVariants.Where(currVariant => IsDifferentVariantWithSameName(variant, currVariant)).ToList(); if (duplicateSpecNames.Any()) { throw new Exception($"{duplicateSpecNames.First().ID} already exists on a variant. Please use unique names for SKUS and try again."); } } } private bool IsDifferentVariantWithSameName(Variant variant, Variant currVariant) { //Do they have the same SKU if (variant.xp.NewID == currVariant.ID) { if (variant.xp.SpecCombo == currVariant.xp.SpecCombo) { //It's most likely the same variant return false; } return true; } return false; } private async Task GetSupplierNameForXpFacet(string supplierID, OrderCloudClient oc, string accessToken) { var supplier = await oc.Suppliers.GetAsync(supplierID, accessToken); return supplier.Name; } private async void HandleSpecOptionChanges(IList requestSpecs, IList existingSpecs, OrderCloudClient oc, string token) { var requestSpecOptions = new Dictionary>(); var existingSpecOptions = new List(); foreach (Spec requestSpec in requestSpecs) { List specOpts = new List(); foreach (SpecOption requestSpecOption in requestSpec.Options) { specOpts.Add(requestSpecOption); } requestSpecOptions.Add(requestSpec.ID, specOpts); } foreach (Spec existingSpec in existingSpecs) { foreach (SpecOption existingSpecOption in existingSpec.Options) { existingSpecOptions.Add(existingSpecOption); } } foreach (var spec in requestSpecOptions) { IList changedSpecOptions = ChangedSpecOptions(spec.Value, existingSpecOptions); await Throttler.RunAsync(changedSpecOptions, 100, 5, option => oc.Specs.SaveOptionAsync(spec.Key, option.ID, option, token)); } } private IList ChangedSpecOptions(List requestOptions, List existingOptions) { return requestOptions.FindAll(requestOption => OptionHasChanges(requestOption, existingOptions)); } private bool OptionHasChanges(SpecOption requestOption, List currentOptions) { var matchingOption = currentOptions.Find(currentOption => currentOption.ID == requestOption.ID); if (matchingOption == null) { return false; }; if (matchingOption.PriceMarkup != requestOption.PriceMarkup) { return true; }; if (matchingOption.IsOpenText != requestOption.IsOpenText) { return true; }; if (matchingOption.ListOrder != requestOption.ListOrder) { return true; }; if (matchingOption.PriceMarkupType != requestOption.PriceMarkupType) { return true; }; return false; } private void ValidateRequestVariant(Variant variant) { if (variant.xp.NewID == variant.ID) { //If NewID is same as ID, no changes have happened so NewID shouldn't be populated. variant.xp.NewID = null; } } private async Task UpdateRelatedPriceSchedules(PriceSchedule updated, OrderCloudClient oc, string token) { var ocAuth = await oc.AuthenticateAsync(); var initial = await oc.PriceSchedules.GetAsync(updated.ID); if (initial.MaxQuantity != updated.MaxQuantity || initial.MinQuantity != updated.MinQuantity || initial.UseCumulativeQuantity != updated.UseCumulativeQuantity || initial.RestrictedQuantity != updated.RestrictedQuantity || initial.ApplyShipping != updated.ApplyShipping || initial.ApplyTax != updated.ApplyTax) { var patch = new PartialPriceSchedule() { MinQuantity = updated.MinQuantity, MaxQuantity = updated.MaxQuantity, UseCumulativeQuantity = updated.UseCumulativeQuantity, RestrictedQuantity = updated.RestrictedQuantity, ApplyShipping = updated.ApplyShipping, ApplyTax = updated.ApplyTax }; var relatedPriceSchedules = await oc.PriceSchedules.ListAllAsync(filters: $"ID={initial.ID}*"); var priceSchedulesToUpdate = relatedPriceSchedules.Where(p => p.ID != updated.ID); await Throttler.RunAsync(priceSchedulesToUpdate, 100, 5, p => { return oc.PriceSchedules.PatchAsync(p.ID, patch, ocAuth.AccessToken); }); } return await oc.PriceSchedules.SaveAsync(updated.ID, updated, token); } private bool HasVariantChange(Variant variant, Variant currVariant) { if (variant.Active != currVariant.Active) { return true; } if (variant.Description != currVariant.Description) { return true; } if (variant.Name != currVariant.Name) { return true; } if (variant.ShipHeight != currVariant.ShipHeight) { return true; } if (variant.ShipLength != currVariant.ShipLength) { return true; } if (variant.ShipWeight != currVariant.ShipWeight) { return true; } if (variant.ShipWidth != currVariant.ShipWidth) { return true; } if (variant?.Inventory?.LastUpdated != currVariant?.Inventory?.LastUpdated) { return true; } if (variant?.Inventory?.QuantityAvailable != currVariant?.Inventory?.QuantityAvailable) { return true; } return false; } #endregion } }