mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-03-19 06:27:15 +00:00
468 lines
18 KiB
C#
468 lines
18 KiB
C#
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<Model.Sku.Price.SkuRepriceInfo> skuInfo;
|
|
private Dictionary<string, Model.Product.CompetitivePrice> competitivePrices;
|
|
DateTime newTimeStamp = 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()
|
|
{
|
|
// 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<int>();
|
|
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<Model.Export.AmazonIventoryLoaderFile>();
|
|
var crDictionary = new Dictionary<string, Model.Sku.Price.PriceInfo>();
|
|
|
|
// 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<string, Model.Sku.Price.SkuRepriceInfo> GetSkuPricingInfo()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the minimum sale price to break even.
|
|
/// </summary>
|
|
/// <param name="i"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <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.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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compares current date and last reprice date to check if SKU can be repriced
|
|
/// </summary>
|
|
/// <param name="lastPriceUpdate">The date & time the SKU was lasted repriced</param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the pricing info to databse
|
|
/// </summary>
|
|
/// <param name="crDictionary">Current repricing data in Dictonary form by SKU number</param>
|
|
private void SaveToDatabase(Dictionary<string, Model.Sku.Price.PriceInfo> 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());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Before running reprice update, this method ensures that the relevant data needed is up to date.
|
|
/// </summary>
|
|
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<string, Model.Sku.Price.PriceInfo> crDictionary)
|
|
{
|
|
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
|
|
var submit = new Logic.Export.AmazonSubmitFile();
|
|
|
|
return; // remove after testing
|
|
|
|
submit.SubmitInventoryLoader(stream);
|
|
}
|
|
}
|
|
} |