Feature repricing min max (#10)

amazon settlement import/export improvements
This commit is contained in:
2020-05-01 09:08:23 +01:00
committed by GitHub
parent 56647c7648
commit 43d61c2ef8
118 changed files with 7930 additions and 3021 deletions

View File

@@ -0,0 +1,386 @@
using CsvHelper;
using System;
using System.Collections.Generic;
using System.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 string sqlConnectionString;
private bnhtrade.Core.Logic.Log.LogEvent log = new Log.LogEvent();
string err = "FbaPricing Error: ";
private string marginSchemeTaxCode = "T190";
int repriceHoldOnSalePeriod = 14; // days
private int newConditionId = Data.Database.Constants.GetProductConditionIdNew();
private List<Model.Sku.Price.SkuPriceParameter> skuInfo;
private Dictionary<string, Model.Product.CompetitivePrice> competitivePrices;
DateTime crTimeStamp = DateTime.UtcNow;
private int repriceIncrementDivisor = 60;
private Dictionary<string, int> saleCountInPeriod = new Dictionary<string, int>();
private Logic.Account.TaxCalculation taxCalc;
private decimal marginSchemeMargin;
public FbaPricing(string sqlConnectionString)
{
this.sqlConnectionString = sqlConnectionString;
taxCalc = new Account.TaxCalculation();
crTimeStamp = DateTime.UtcNow;
marginSchemeMargin = taxCalc.GetMarginMultiplier(crTimeStamp);
}
public void Update(bool overrideDayCheck = false)
{
using (var scope = new TransactionScope())
{
string orderChannel = "Amazon.co.uk"; // may in future enable other order channels
// need to add some cheks up here for last stock reconcilliation
// get current sku base pricing details (stock quantity, competative price, VAT info, etc.)
skuInfo = new Data.Database.Sku.Price.ReadParameter(sqlConnectionString).Execute();
if (skuInfo == null || !skuInfo.Any())
{
throw new Exception("Querying the database returned no records.");
}
// create lists that we'll add to during lopp
var loader = new List<Model.Export.AmazonIventoryLoaderFile>();
var crDictionary = new Dictionary<string, Model.Sku.Price.PriceInfo>();
// open needed classes
var readAge = new Data.Database.Import.ReadFbaInventoryAge(sqlConnectionString);
// get current db pricing
var dbDictionary = new Data.Database.Sku.Price.ReadPricingDetail(sqlConnectionString).ReadDictionary(skuInfo.Select(o => o.SkuNumber).ToList(), orderChannel);
// get required competivie prices
var readComp = new Logic.Product.GetCompetitivePrice(sqlConnectionString);
readComp.EstimatePrice = true;
var compPrices = readComp.Execute(skuInfo);
// loop through skus returnd from stock query
for (int i = 0; i < skuInfo.Count(); 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 = crTimeStamp;
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 = GetMinPriceCost(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
// validate and save values to database
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(sqlConnectionString).Executue(crDictionary.Values.ToList());
// create and upload inventory loader file to amazon
var exportList = new List<Model.Export.AmazonIventoryLoaderFile>();
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 mws
var submit = new Logic.Export.AmazonSubmitFile(sqlConnectionString);
return;
submit.SubmitInventoryLoader(stream);
scope.Complete();
}
}
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(crTimeStamp.Year, crTimeStamp.Month, crTimeStamp.Day);
// will only update once on tue, wed or thurs each week.
if (today.DayOfWeek == DayOfWeek.Tuesday || today.DayOfWeek == DayOfWeek.Wednesday || today.DayOfWeek == DayOfWeek.Thursday)
{
if (today > lastPriceUpdate.AddDays(3))
{
update = true;
}
}
return update;
}
/// <summary>
/// Get the minimum sale price to break even.
/// </summary>
/// <param name="i"></param>
/// <returns></returns>
private decimal GetMinPriceCost(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;
}
/// <summary>
/// Get the minimum sale price to achieve required profit margin.
/// </summary>
/// <param name="i"></param>
/// <returns>Minimum price in decimal</returns>
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.ReadFbaSaleShipment(sqlConnectionString)
.GetSaleCount(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;
}
}
}
}