using CsvHelper; using System; using System.Collections.Generic; using Microsoft.Data.SqlClient; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Transactions; namespace bnhtrade.Core.Logic.Sku.Price { public class FbaPricing { private bnhtrade.Core.Logic.Log.LogEvent log = new Log.LogEvent(); string err = "FbaPricing Error: "; private string marginSchemeTaxCode = Data.Database.Constants.GetMarginSchemeTaxCode(); private int newConditionId = (int)Data.Database.Constants.SkuCondition.New; private List skuInfo; private Dictionary competitivePrices; DateTime newTimeStamp = DateTime.UtcNow; private int repriceIncrementDivisor = 60; private Dictionary saleCountInPeriod = new Dictionary(); private Logic.Account.TaxCalculation taxCalc; private decimal marginSchemeMargin; public FbaPricing() { // was part of a fba repricing feature, that, for now, is being abandoned throw new NotImplementedException(); taxCalc = new Account.TaxCalculation(); newTimeStamp = DateTime.UtcNow; marginSchemeMargin = taxCalc.GetMarginMultiplier(newTimeStamp); } public int RepriceHoldOnSalePeriod { get; set; } = 14; // days public void Update(bool overrideDayCheck = false) { UpdatePrecheck(); using (var scope = new TransactionScope()) { string orderChannel = Data.Database.Constants.GetOrderChannelAmazonUk(); ; // may in future enable other order channels // get SKU quantities in stock on FBA var statusTypeList = new List(); statusTypeList.Add((int)Data.Database.Constants.StockStatusType.FbaInventoryActive); statusTypeList.Add((int)Data.Database.Constants.StockStatusType.FbaShipment); var fbaSkuStock = new Logic.Stock.StatusTypeBalance().GetByStatusTypeId(statusTypeList); // retrive SKU info var getSku = new Logic.Sku.GetSkuInfo(); var skuInfoDict = getSku.ConvertToDictionary(getSku.BySkuNumber(fbaSkuStock.Keys.ToList())); // pull current sku base pricing details (stock quantity, competative price, VAT info, etc.) from the database skuInfo = new Data.Database.Sku.Price.ReadParameter().Execute(); if (skuInfo == null || !skuInfo.Any()) { err += "Querying the database returned no records."; log.LogError(err); throw new Exception(err); } // create lists that we'll add to during lopp var loader = new List(); var crDictionary = new Dictionary(); // instanlise needed classes var readAge = new Data.Database.Import.AmazonFbaInventoryAgeRead(); // get current pricing from database var dbDictionary = new Data.Database.Sku.Price.ReadPricingDetail().ReadDictionary(skuInfo.Select(o => o.SkuNumber).ToList(), orderChannel); // get required competivie prices var readComp = new Logic.Product.GetCompetitivePrice(); readComp.EstimatePrice = true; var compPrices = readComp.Execute(skuInfo); // loop through sku pricing returned from database for (int i = 0; i < skuInfo.Count(); i++) { var existing = skuInfo[i]; string skuNumber = skuInfo[i].SkuNumber; var cr = new Model.Sku.Price.PriceInfo(); if (!overrideDayCheck && dbDictionary.Count > 0 && !OkayToReprice(dbDictionary[skuNumber].PriceInfoTimeStamp)) { continue; } // load in values from skuInfo cr.PriceTypeId = 1; cr.ReviewRequired = false; cr.OrderChannel = orderChannel; cr.OrderChannelQuantity = skuInfo[i].TotalQuantity; cr.PriceInfoTimeStamp = newTimeStamp; cr.SkuNumber = skuInfo[i].SkuNumber; // get inventory age range var invAge = readAge.BySkuNumber(skuInfo[i].SkuNumber, orderChannel); if (invAge == null) { // this means lost stock, or unreconciled inventory... need to pause these skus else the price could decrease without it being on sale err += "No records returned from tblImportFbaInventoryAgeReport for skuID=" + skuInfo[i].SkuId; log.LogError(err); throw new Exception(err); } cr.InventoryAgeMin = invAge.Value.MinAge; cr.InventoryAgeMax = invAge.Value.MaxAge; // get minimum prices cr.UnitMinPriceCost = GetPriceBreakEven(i); cr.UnitMinPriceProfit = GetMinPriceProfit(i); cr.UnitPurchaseCost = skuInfo[i].UnitCostAverage; // set competitive price if (compPrices.ContainsKey(skuInfo[i].SkuNumber)) { cr.CompetitivePrice = compPrices[skuInfo[i].SkuNumber].Price; cr.CompetitivePriceIsEstimated = compPrices[skuInfo[i].SkuNumber].PriceIsEstimated; } else { cr.CompetitivePrice = cr.UnitMinPriceProfit + (cr.UnitMinPriceProfit / 2); cr.CompetitivePriceIsEstimated = true; } // set min max price cr.RepriceIncrement = cr.CompetitivePrice / repriceIncrementDivisor; if (dbDictionary.ContainsKey(skuNumber)) { // sales wihtin period, therefore hold price if (GetSaleCountInPeriod(i) > 0) { cr.MaxPrice = dbDictionary[skuNumber].MaxPrice; cr.MinPrice = dbDictionary[skuNumber].MinPrice; cr.RepriceIncrement = dbDictionary[skuNumber].RepriceIncrement; } // else reduce else { if (dbDictionary[skuNumber].MaxPrice < dbDictionary[skuNumber].MinPrice) { err += "Max price lower than min price"; log.LogError(err); throw new Exception(err); } // redue both prices together else if (dbDictionary[skuNumber].MaxPrice == dbDictionary[skuNumber].MinPrice) { cr.MaxPrice = dbDictionary[skuNumber].MaxPrice - cr.RepriceIncrement; cr.MinPrice = cr.MaxPrice; } // reduce only max until it hits the min else { cr.MaxPrice = dbDictionary[skuNumber].MaxPrice - cr.RepriceIncrement; if (cr.MaxPrice < dbDictionary[skuNumber].MinPrice) { cr.MinPrice = cr.MaxPrice; } else { cr.MinPrice = dbDictionary[skuNumber].MinPrice; } } } } else { // new value cr.MaxPrice = cr.CompetitivePrice * 1.2m; cr.MinPrice = cr.CompetitivePrice * 1m; } // check on min price cr.UnitMinPriceDestory = GetMinPriceDestroy(i); if (cr.MaxPrice < cr.UnitMinPriceDestory) { cr.MaxPrice = cr.UnitMinPriceDestory; } if (cr.MinPrice < cr.UnitMinPriceDestory) { cr.MinPrice = cr.UnitMinPriceDestory; } // add values to inventory loader list var item = new Model.Export.AmazonIventoryLoaderFile(); item.Sku = skuInfo[i].SkuNumber; item.Price = cr.MaxPrice; item.MinimumAllowedPrice = cr.MinPrice; item.MaximumAllowedPrice = cr.MaxPrice; item.SetFulfillmentCenterId(true); loader.Add(item); // save current prices to dictionary if (crDictionary.ContainsKey(skuNumber)) { err += "Multiple SkuId found in data"; log.LogError(err); throw new Exception(err); } crDictionary.Add(skuNumber, cr); } // finish loop // save values to database SaveToDatabase(crDictionary); // upload to amazon UploadToAmazon(crDictionary); return; // remove after testing scope.Complete(); } } private Dictionary GetSkuPricingInfo() { throw new NotImplementedException(); } /// /// Get the minimum sale price to break even. /// /// /// private decimal GetPriceBreakEven(int i) { decimal costPrice = skuInfo[i].UnitCostAverage; decimal agentFeeFixed = skuInfo[i].AgentFeeFixed; decimal agentFeeMargin = skuInfo[i].AgentFeeMargin; decimal vatMargin = skuInfo[i].VatMargin; decimal price = 0; if (skuInfo[i].TaxCode == marginSchemeTaxCode) { price = (costPrice + agentFeeFixed - (costPrice * marginSchemeMargin)) / (1 - agentFeeMargin - marginSchemeMargin); } else { price = (costPrice + agentFeeFixed) / (1 - agentFeeMargin - vatMargin); } price = decimal.Round(price, 2); return price; } /// /// Get the minimum sale price to achieve required profit margin. /// /// /// Minimum price in decimal private decimal GetMinPriceProfit(int i) { decimal costPrice = skuInfo[i].UnitCostAverage; decimal minProfit = skuInfo[i].PriceMinProfit; decimal profitMargin = skuInfo[i].ProfitMargin; decimal agentFeeFixed = skuInfo[i].AgentFeeFixed; decimal agentFeeMargin = skuInfo[i].AgentFeeMargin; decimal vatMargin = skuInfo[i].VatMargin; decimal price = 0; if (skuInfo[i].TaxCode == marginSchemeTaxCode) // taxcodeinfo now has ismarginscheme boolean { price = (costPrice + agentFeeFixed - (costPrice * marginSchemeMargin)) / (1 - profitMargin - agentFeeMargin - marginSchemeMargin); } else { price = (costPrice + agentFeeFixed) / (1 - profitMargin - agentFeeMargin - vatMargin); } price = decimal.Round(price, 2); // if profit margin is less than min required, redo using min value (not percent) if (price < minProfit) { if (skuInfo[i].TaxCode == marginSchemeTaxCode) { price = (minProfit + costPrice + agentFeeFixed - (costPrice * marginSchemeMargin)) / (1 - agentFeeMargin - marginSchemeMargin); } else { price = (minProfit + costPrice + agentFeeFixed) / (1 - agentFeeMargin - vatMargin); } } price = decimal.Round(price, 2); return price; } private decimal GetMinPriceDestroy(int i) { decimal agentFeeFixed = skuInfo[i].AgentFeeFixed; decimal agentFeeMargin = skuInfo[i].AgentFeeMargin; decimal vatMargin = skuInfo[i].VatMargin; decimal price = 0; if (skuInfo[i].TaxCode == marginSchemeTaxCode) { price = (agentFeeFixed) / (1 - agentFeeMargin); } else { price = (agentFeeFixed) / (1 - agentFeeMargin - vatMargin); } price = decimal.Round(price, 2); return price; } private int GetSaleCountInPeriod(int i) { if (!saleCountInPeriod.Any()) { saleCountInPeriod = new Data.Database.Import.AmazonFbaSaleShipment() .ReadSaleCount(skuInfo.Select(x => x.SkuNumber).ToList(), DateTime.Now.AddDays(RepriceHoldOnSalePeriod * -1), DateTime.Now); } if (saleCountInPeriod.ContainsKey(skuInfo[i].SkuNumber)) { return saleCountInPeriod[skuInfo[i].SkuNumber]; } else { return 0; } } /// /// Compares current date and last reprice date to check if SKU can be repriced /// /// The date & time the SKU was lasted repriced /// private bool OkayToReprice(DateTime lastPriceUpdate) { if (lastPriceUpdate == default(DateTime)) { err += "Invalid, datetime is default."; log.LogError(err); throw new Exception(err); } bool update = false; lastPriceUpdate = new DateTime(lastPriceUpdate.Year, lastPriceUpdate.Month, lastPriceUpdate.Day); DateTime today = new DateTime(newTimeStamp.Year, newTimeStamp.Month, newTimeStamp.Day); // will only update once on tue, wed or thurs each week. <------- Why??? don't know what my reasoning was for this if (today.DayOfWeek == DayOfWeek.Tuesday || today.DayOfWeek == DayOfWeek.Wednesday || today.DayOfWeek == DayOfWeek.Thursday) { if (today > lastPriceUpdate.AddDays(3)) { update = true; } } return update; } /// /// Saves the pricing info to databse /// /// Current repricing data in Dictonary form by SKU number private void SaveToDatabase(Dictionary crDictionary) { var validate = new Core.Logic.Validate.SkuPriceInfo(); if (!validate.IsValidDatabaseCreate(crDictionary.Values.ToList())) { err += "Database object create validation failed"; log.LogError(err, validate.ValidationResultListToString()); throw new Exception(err); } new Data.Database.Sku.Price.CreatePricingDetail().Executue(crDictionary.Values.ToList()); } /// /// Before running reprice update, this method ensures that the relevant data needed is up to date. /// private void UpdatePrecheck() { throw new NotImplementedException(); // check last FBA sale import err += "Querying the database returned no records."; log.LogError(err); throw new Exception(err); // check last amazon sku fees updates } private void UploadToAmazon(Dictionary crDictionary) { var exportList = new List(); foreach (var item in crDictionary.Values) { var listItem = new Model.Export.AmazonIventoryLoaderFile(); listItem.Sku = item.SkuNumber; listItem.MinimumAllowedPrice = item.MinPrice; listItem.MaximumAllowedPrice = item.MaxPrice; listItem.Price = item.MaxPrice; listItem.SetFulfillmentCenterId(true); exportList.Add(listItem); } // validate var vaildateInvLoader = new Validate.AmazonIventoryLoaderFile(); if (!vaildateInvLoader.IsValidFbaPricing(exportList)) { err += "Inventory loader object validation failed"; log.LogError(err, vaildateInvLoader.ValidationResultListToString()); throw new Exception(err); } // create file stream var config = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.CurrentCulture); config.Delimiter = "\t"; config.Encoding = Encoding.UTF8; var stream = new MemoryStream(); using (var writer = new StreamWriter(stream, Encoding.UTF8)) using (var csv = new CsvWriter(writer, config)) { csv.WriteRecords(exportList); } // submit file to database and amazon var submit = new Logic.Export.AmazonSubmitFile(); return; // remove after testing submit.SubmitInventoryLoader(stream); } } }