mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-03-21 07:17:15 +00:00
Added invoice export function and started implementation of unitofwork pattern (#43)
* complete read invoices from db * wip * wip * wip * wip * wip * wip * wip * wip * updated nuget package spapi * WIP * wip, now test * wip, jut need to fix tax inclusive line amounts not supported * wip * wip, before I f everything up * no, it complies now, this is the one before I f everything up * wip * wip * wip, logic ready for testing * wip it builds!!!! * wip tested, working, need to complete the gui section * wip * wip * wip - created export invoice data delete, time for testing * wip testing phase * wip - delete function fully tested and working * wip on to sorting out the issue with settlement invoices not tallying * wip * wip * wip * wip * wip before I complete change the ReadInvoiceLineItem sections * that appears to have worked, on with the main quest * no it's doesn't work, saving before i remove the confusing cache system (just use a dictionary!!) * wipping picadilli * wip * wip * implemented uow on inovice export, now for testing * wip * wip all tested do invoice currency convertion fearure * wip * pretty much done so long as xero accepts the exported invoices * Complete!
This commit is contained in:
445
src/bnhtrade.Core/Logic/Account/CurrencyService.cs
Normal file
445
src/bnhtrade.Core/Logic/Account/CurrencyService.cs
Normal file
@@ -0,0 +1,445 @@
|
||||
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 newLineSum = 0;
|
||||
int i = 0;
|
||||
foreach (var line in invoice.InvoiceLineList)
|
||||
{
|
||||
decimal weighting = line.LineTotalAmount / invoiceOriginalTotalAmount;
|
||||
decimal lineCalculatedTotal = invoiceOriginalTotalAmount * weighting;
|
||||
lineWeighting.Add(new(i, weighting));
|
||||
lineCalculatedTotalByWeighting.Add(new(i, lineCalculatedTotal));
|
||||
|
||||
// edit line
|
||||
if (line.TaxAmountAdjust != 0)
|
||||
{
|
||||
line.TaxAmountAdjust = existingRate.ConvertToGbp(line.TaxAmountAdjust);
|
||||
}
|
||||
line.UnitAmount = null;
|
||||
line.SetUnitAmountByLineTotal(decimal.Round(lineCalculatedTotal, 2));
|
||||
newLineSum += 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 (invoiceOriginalTotalAmount != newLineSum)
|
||||
{
|
||||
decimal amountRemainingToDistribute = invoiceOriginalTotalAmount - newLineSum;
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user