Files
bnhtrade/src/bnhtrade.Core/Logic/Account/CurrencyService.cs

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.");
}
}
}
}