mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-03-19 06:27:15 +00:00
Amazon had depreciated a number of reports that were used for stock reconciliation. Application now uses the new fba ledger report to reconcile. It is currently untested, as this requires data from Amazon. Methods that require testing will return a 'NotImplementedException'. Also, removed the depreciated ILMerge and replaced with ILRepack. Plus much more tidying up, and improvements.
468 lines
18 KiB
C#
468 lines
18 KiB
C#
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 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.GetStatusTypeBalance().BySku(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);
|
|
}
|
|
}
|
|
} |