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 _exchangeRateList = new List(); 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)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 { 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)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 GetExchangeRateObjectList(List 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."); } } } }