mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-03-19 06:27:15 +00:00
446 lines
18 KiB
C#
446 lines
18 KiB
C#
using bnhtrade.Core.Data.Database.UnitOfWork;
|
|
using FikaAmazonAPI.AmazonSpApiSDK.Models.FulfillmentInbound;
|
|
using FikaAmazonAPI.ConstructFeed.Messages;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Data.SqlClient;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml.Linq;
|
|
using static bnhtrade.Core.Model.Account.PurchaseInvoice;
|
|
|
|
namespace bnhtrade.Core.Logic.Account
|
|
{
|
|
public class CurrencyService
|
|
{
|
|
private readonly IUnitOfWork _providedUnitOfWork = null;
|
|
private readonly bool _ownsUnitOfWork = false;
|
|
private List<Model.Account.CurrencyExchangeRate> _exchangeRateList = new List<Model.Account.CurrencyExchangeRate>();
|
|
|
|
Log.LogEvent _log = new Log.LogEvent();
|
|
|
|
public string ErrorMessage { get; private set; } = null;
|
|
|
|
public bool ErrorMessageIsSet
|
|
{
|
|
get
|
|
{
|
|
return !string.IsNullOrEmpty(ErrorMessage);
|
|
}
|
|
}
|
|
|
|
public CurrencyService()
|
|
{
|
|
_ownsUnitOfWork = true;
|
|
}
|
|
|
|
internal CurrencyService(IUnitOfWork unitOfWork)
|
|
{
|
|
_providedUnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
|
|
_ownsUnitOfWork = false;
|
|
}
|
|
|
|
private void Init()
|
|
{
|
|
ErrorMessage = null;
|
|
}
|
|
|
|
public bool ConvertInvoiceToGbp(Model.Account.IInvoice invoice)
|
|
{
|
|
Init();
|
|
|
|
//checks
|
|
if (invoice == null)
|
|
{
|
|
ErrorMessage = "Invoice cannot be null";
|
|
return false;
|
|
}
|
|
if (invoice.InvoiceCurrencyCode == Model.Account.CurrencyCode.GBP)
|
|
{
|
|
return true; // no conversion needed
|
|
}
|
|
if (invoice.InvoiceDate == null)
|
|
{
|
|
ErrorMessage = "Invoice date cannot be null";
|
|
return false;
|
|
}
|
|
|
|
//validate the invoice
|
|
var validate = new Logic.Validate.Invoice();
|
|
// Fix for CS1503: Cast 'invoice' to 'Model.Account.Invoice' explicitly
|
|
if (validate.IsValidExportInvoice(new List<Model.Account.Invoice> { (Model.Account.Invoice)invoice }) == false)
|
|
{
|
|
ErrorMessage = "Invoice failed validation. See logs for further details.";
|
|
_log.LogError(ErrorMessage);
|
|
return false;
|
|
}
|
|
|
|
// check if exchange rate is already in list
|
|
Model.Account.CurrencyExchangeRate existingRate = null;
|
|
foreach (var exchangeRate in _exchangeRateList)
|
|
{
|
|
if (exchangeRate.CurrencyCode == invoice.InvoiceCurrencyCode && exchangeRate.DateTimeWithinPeriodCheck(invoice.InvoiceDate.Value))
|
|
{
|
|
existingRate = exchangeRate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// get rates from db and add to field list if not in list already
|
|
var rateList = GetExchangeRateObjectList(
|
|
new List<Model.Account.CurrencyCode> { invoice.InvoiceCurrencyCode }, invoice.InvoiceDate.Value);
|
|
|
|
if (rateList == null || rateList.Count == 0)
|
|
{
|
|
ErrorMessage =
|
|
"Exchange rate for currency code '" + invoice.InvoiceCurrencyCode + "' and date " + invoice.InvoiceDate.Value.ToShortDateString() + "' does not exist in the Exchange Rate table";
|
|
return false;
|
|
}
|
|
|
|
_exchangeRateList.AddRange(rateList);
|
|
existingRate = rateList[0];
|
|
|
|
// do the conversion
|
|
// convert header information
|
|
decimal invoiceOriginalTotalAmount = invoice.InvoiceTotalAmount.Value;
|
|
invoice.InvoiceTotalAmount = existingRate.ConvertToGbp(invoice.InvoiceTotalAmount.Value);
|
|
invoice.InvoiceCurrencyCode = Model.Account.CurrencyCode.GBP;
|
|
|
|
// convert line items
|
|
var lineWeighting = new List<(int, decimal)>();
|
|
var lineCalculatedTotalByWeighting = new List<(int, decimal)>();
|
|
decimal convertedLineListSum = 0;
|
|
int i = 0;
|
|
foreach (var line in invoice.InvoiceLineList)
|
|
{
|
|
decimal weighting = line.LineTotalAmount / invoiceOriginalTotalAmount;
|
|
decimal lineConvertedTotal = existingRate.ConvertToGbp(invoiceOriginalTotalAmount * weighting);
|
|
lineWeighting.Add(new(i, weighting));
|
|
lineCalculatedTotalByWeighting.Add(new(i, lineConvertedTotal));
|
|
|
|
// edit line
|
|
if (line.TaxAmountAdjust != 0)
|
|
{
|
|
line.TaxAmountAdjust = existingRate.ConvertToGbp(line.TaxAmountAdjust);
|
|
}
|
|
line.UnitAmount = null;
|
|
line.SetUnitAmountByLineTotal(decimal.Round(lineConvertedTotal, 2));
|
|
convertedLineListSum += line.LineTotalAmount;
|
|
|
|
i++;
|
|
}
|
|
|
|
// there may be rounding errors
|
|
// section untested - may have to fix some bugs in here when I've got some invoice to test on
|
|
if (invoice.InvoiceTotalAmount != convertedLineListSum)
|
|
{
|
|
decimal amountRemainingToDistribute = invoice.InvoiceTotalAmount.Value - convertedLineListSum;
|
|
lineWeighting = lineWeighting.OrderByDescending(i => i.Item2).ToList();
|
|
|
|
foreach (var line in lineWeighting)
|
|
{
|
|
// distribute the amount remaining to the lines
|
|
decimal amountToDistribute = amountRemainingToDistribute * line.Item2;
|
|
if (amountToDistribute > 0 && amountToDistribute < 0.01m)
|
|
{
|
|
amountToDistribute = 0.01m; // minimum amount to distribute
|
|
}
|
|
else if (amountToDistribute < 0 && amountToDistribute > -0.01m)
|
|
{
|
|
amountToDistribute = -0.01m; // minimum amount to distribute
|
|
}
|
|
else
|
|
{
|
|
amountToDistribute = Math.Round(amountToDistribute, 2);
|
|
}
|
|
|
|
amountRemainingToDistribute = amountRemainingToDistribute - amountToDistribute;
|
|
invoice.InvoiceLineList[line.Item1].SetUnitAmountByLineTotal(
|
|
invoice.InvoiceLineList[line.Item1].LineTotalAmount + amountToDistribute
|
|
);
|
|
|
|
if (amountRemainingToDistribute == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (amountRemainingToDistribute > 0.01m || amountRemainingToDistribute < -0.01m)
|
|
{
|
|
// check if the amount remaining to distribute is too large
|
|
// this should not happen, but if it does, you'll have to manually fix the invoice or do more coding
|
|
ErrorMessage = "Rounding error when converting invoice to GBP. Amount remaining to distribute: " + amountRemainingToDistribute.ToString("C", CultureInfo.CurrentCulture);
|
|
_log.LogError(ErrorMessage);
|
|
return false;
|
|
}
|
|
else if (amountRemainingToDistribute != 0 && amountRemainingToDistribute < 0.01m && amountRemainingToDistribute > -0.01m)
|
|
{
|
|
invoice.InvoiceLineList[lineWeighting[0].Item1].SetUnitAmountByLineTotal(
|
|
invoice.InvoiceLineList[lineWeighting[0].Item1].LineTotalAmount + amountRemainingToDistribute
|
|
);
|
|
}
|
|
}
|
|
|
|
// Fix for CS1950: Ensure the list contains valid 'Model.Account.Invoice' objects
|
|
if (validate.IsValidExportInvoice(new List<Model.Account.Invoice> { (Model.Account.Invoice)invoice }) == false)
|
|
{
|
|
ErrorMessage = "Invoice failed validation after conversion to GBP. See logs for further details.";
|
|
_log.LogError(ErrorMessage);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public List<Model.Account.CurrencyExchangeRate> GetExchangeRateObjectList(List<Model.Account.CurrencyCode> currencyCodeList, DateTime conversionDate)
|
|
{
|
|
Init();
|
|
|
|
IUnitOfWork currentUow = null;
|
|
if (_ownsUnitOfWork)
|
|
{
|
|
currentUow = new UnitOfWork();
|
|
}
|
|
else
|
|
{
|
|
currentUow = _providedUnitOfWork;
|
|
}
|
|
|
|
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
|
|
{
|
|
return currentUow.CurrencyRepository.ReadExchangeRate(currencyCodeList, conversionDate);
|
|
}
|
|
}
|
|
|
|
public decimal CurrencyConvertToGbp(string currencyCode, decimal amount, DateTime conversionDate)
|
|
{
|
|
Init();
|
|
|
|
if (string.IsNullOrEmpty(currencyCode) || currencyCode.Length != 3)
|
|
{
|
|
throw new Exception("Invalid currency code '" + currencyCode + "'");
|
|
}
|
|
|
|
Enum.TryParse(currencyCode, out Model.Account.CurrencyCode enumCurrencyCode);
|
|
|
|
return CurrencyConvertToGbp(enumCurrencyCode, amount, conversionDate);
|
|
}
|
|
|
|
public decimal CurrencyConvertToGbp(Model.Account.CurrencyCode currencyCode, decimal amount, DateTime conversionDate)
|
|
{
|
|
Init();
|
|
|
|
//checks
|
|
if (currencyCode == Model.Account.CurrencyCode.GBP || amount == 0M)
|
|
{
|
|
return amount;
|
|
}
|
|
|
|
// read db
|
|
IUnitOfWork currentUow = null;
|
|
if (_ownsUnitOfWork)
|
|
{
|
|
currentUow = new UnitOfWork();
|
|
}
|
|
else
|
|
{
|
|
currentUow = _providedUnitOfWork;
|
|
}
|
|
|
|
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
|
|
{
|
|
var exchageRate = currentUow.CurrencyRepository.ReadExchangeRate(currencyCode, conversionDate);
|
|
|
|
if (exchageRate != null)
|
|
{
|
|
return amount / Convert.ToDecimal(exchageRate);
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Currency code '" + currencyCode + "' or date " + conversionDate.ToShortDateString() + " " + conversionDate.ToLongTimeString() + "' does not exist in the Exchange Rate table");
|
|
}
|
|
}
|
|
}
|
|
|
|
private DateTime GetHmrcMaxPeriodAvaible()
|
|
{
|
|
// HMRC monthly exchange rates are published on the penultimate Thursday of the month before
|
|
// For some leeeeeeeeway we'll use the penultimate Friday
|
|
|
|
// find penultimate Friday for current month
|
|
var ukTimeNow = new Logic.Utilities.DateTime().ConvertUtcToUk(DateTime.UtcNow);
|
|
var monthDayCount = DateTime.DaysInMonth(ukTimeNow.Year, ukTimeNow.Month);
|
|
var thisMonthPenultimateFriday = DateTime.SpecifyKind(new DateTime(ukTimeNow.Year, ukTimeNow.Month, monthDayCount), DateTimeKind.Unspecified);
|
|
int count = 0;
|
|
int fridayCount = 0;
|
|
while (count != 15)
|
|
{
|
|
if (thisMonthPenultimateFriday.DayOfWeek == DayOfWeek.Friday)
|
|
{
|
|
fridayCount++;
|
|
if (fridayCount == 2)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
thisMonthPenultimateFriday = thisMonthPenultimateFriday.AddDays(-1);
|
|
count++;
|
|
}
|
|
|
|
if (count == 15)
|
|
{
|
|
throw new Exception("Something went wrong here ErrorID:ef7f5d8f-0f7b-4014-aa65-421ecd5d7367");
|
|
}
|
|
|
|
var mostRecentPeriodAvaible = DateTime.SpecifyKind(new DateTime(ukTimeNow.Year, ukTimeNow.Month, 1), DateTimeKind.Unspecified); ;
|
|
if (ukTimeNow >= thisMonthPenultimateFriday)
|
|
{
|
|
mostRecentPeriodAvaible = mostRecentPeriodAvaible.AddMonths(1);
|
|
}
|
|
|
|
return mostRecentPeriodAvaible;
|
|
}
|
|
|
|
public void UpdateHmrcExchageRates()
|
|
{
|
|
Init();
|
|
|
|
_log.LogInformation("Starting update database HMRC exchange rates");
|
|
|
|
int exchangeRateSourceId = 1; // id for hmrc
|
|
|
|
// retrive most recent data from db
|
|
IUnitOfWork currentUow = null;
|
|
if (_ownsUnitOfWork)
|
|
{
|
|
currentUow = new UnitOfWork();
|
|
}
|
|
else
|
|
{
|
|
currentUow = _providedUnitOfWork;
|
|
}
|
|
|
|
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
|
|
{
|
|
var dbLatestRates = currentUow.CurrencyRepository.ReadExchangeRateLatest();
|
|
|
|
// sanity check, make sure there are no duplicates
|
|
int count = 0;
|
|
foreach (var exchageRate in dbLatestRates)
|
|
{
|
|
count = 0;
|
|
var currency = exchageRate.CurrencyCode;
|
|
foreach (var subExchageRate in dbLatestRates)
|
|
{
|
|
if (exchageRate.CurrencyCode == subExchageRate.CurrencyCode)
|
|
{
|
|
count = 1;
|
|
}
|
|
}
|
|
if (count > 1)
|
|
{
|
|
throw new FormatException("Datebase returned duplicate information");
|
|
}
|
|
}
|
|
|
|
// test for no data (first time running)
|
|
var hmrcMonthToRetrive = new DateTime();
|
|
if (dbLatestRates.Any() == false)
|
|
{
|
|
hmrcMonthToRetrive = Data.Database.Constants.GetBusinessStartUk();
|
|
}
|
|
|
|
// set first/earliest month to retrive from hmrc website
|
|
foreach (var exchageRate in dbLatestRates)
|
|
{
|
|
var dbEndDateTime = exchageRate.DateTimeEndUk;
|
|
|
|
if (hmrcMonthToRetrive == default(DateTime))
|
|
{
|
|
hmrcMonthToRetrive = dbEndDateTime;
|
|
}
|
|
else
|
|
{
|
|
if (dbEndDateTime < hmrcMonthToRetrive)
|
|
{
|
|
hmrcMonthToRetrive = dbEndDateTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check - more coding required to retrive periods before 2021-01-01
|
|
if (hmrcMonthToRetrive < DateTime.SpecifyKind(new DateTime(2021, 1, 1), DateTimeKind.Unspecified))
|
|
{
|
|
throw new Exception("This function does not currently retirve exchange rates from HMRC for dates before 2021-01-01");
|
|
}
|
|
|
|
// check if retrival from hmrc is required
|
|
var hmrcMaxMonthAvaible = GetHmrcMaxPeriodAvaible();
|
|
if (hmrcMonthToRetrive.Year == hmrcMaxMonthAvaible.Year && hmrcMonthToRetrive.Month > hmrcMaxMonthAvaible.Month)
|
|
{
|
|
// nothing to retrive
|
|
_log.LogInformation("Exchange rates curretly up to date, exiting.");
|
|
return;
|
|
}
|
|
|
|
// get info from hmrc and insert data in db
|
|
while (hmrcMonthToRetrive <= hmrcMaxMonthAvaible)
|
|
{
|
|
count = 0;
|
|
|
|
var url = new string(
|
|
"https://www.trade-tariff.service.gov.uk/api/v2/exchange_rates/files/monthly_xml_"
|
|
+ hmrcMonthToRetrive.Year.ToString()
|
|
+ "-"
|
|
+ hmrcMonthToRetrive.Month.ToString()
|
|
+ ".xml"
|
|
);
|
|
var xd = new XDocument();
|
|
xd = XDocument.Load(url);
|
|
|
|
foreach (var exchageRate in dbLatestRates)
|
|
{
|
|
if (exchageRate.DateTimeStartUtc < hmrcMonthToRetrive)
|
|
{
|
|
//retrive exchange rate from xml
|
|
XElement node = xd.Root.Elements("exchangeRate").Where(e => e.Element("currencyCode").Value == exchageRate.CurrencyCode.ToString()).FirstOrDefault();
|
|
decimal rate = decimal.Parse(node.Element("rateNew").Value);
|
|
rate = decimal.Round(rate, 4);
|
|
|
|
// insert into db
|
|
currentUow.CurrencyRepository.InsertExchangeRate(
|
|
exchangeRateSourceId
|
|
, exchageRate.CurrencyCode
|
|
, rate
|
|
, new Utilities.DateTime().ConvertUkToUtc(hmrcMonthToRetrive)
|
|
, new Utilities.DateTime().ConvertUkToUtc(hmrcMonthToRetrive.AddMonths(1))
|
|
);
|
|
|
|
count++;
|
|
}
|
|
}
|
|
|
|
_log.LogInformation(
|
|
count + " new exchange rate(s) added to database for " + hmrcMonthToRetrive.ToString("MMMM") + " " + hmrcMonthToRetrive.Year.ToString()
|
|
);
|
|
|
|
hmrcMonthToRetrive = hmrcMonthToRetrive.AddMonths(1);
|
|
}
|
|
|
|
if (_ownsUnitOfWork)
|
|
{
|
|
currentUow.Commit();
|
|
}
|
|
|
|
_log.LogInformation("Updating database currency exchange rates complete.");
|
|
}
|
|
}
|
|
}
|
|
}
|