Bug fix in ExportSalesInvoice and converted more code over to repository and service pattern

This commit is contained in:
2025-07-07 15:22:21 +01:00
parent 5900a6e6e4
commit 5cd653d700
64 changed files with 2623 additions and 2517 deletions

View File

@@ -1,5 +1,6 @@
using Amazon.SQS.Model.Internal.MarshallTransformations;
using bnhtrade.Core.Data.Database.UnitOfWork;
using bnhtrade.Core.Model.Account;
using bnhtrade.Core.Model.Amazon;
using bnhtrade.Core.Test.Export;
using FikaAmazonAPI.ReportGeneration.ReportDataTable;
@@ -17,24 +18,18 @@ using static bnhtrade.Core.Model.Import.AmazonSettlement;
namespace bnhtrade.Core.Logic.Export.AccountInvoice
{
internal class AmazonSettlement : Data.Database.Connection
internal class AmazonSettlement : UnitOfWorkBase
{
private readonly IUnitOfWork _providedUnitOfWork = null;
private readonly bool _ownsUnitOfWork = false;
private Logic.Log.LogEvent _log = new Logic.Log.LogEvent();
private List<string> _lineItemCodeList = new List<string>();
private bool _settlementAmountIsTaxExclusive = false; // i.e. they're tax inclusive
public AmazonSettlement()
{
_ownsUnitOfWork = true;
}
internal AmazonSettlement(IUnitOfWork unitOfWork)
internal AmazonSettlement(IUnitOfWork unitOfWork) : base(unitOfWork)
{
_providedUnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
_ownsUnitOfWork = false;
}
public string ErrorMessage { get; private set; }
@@ -53,34 +48,34 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
Init();
_log.LogInformation("Starting processing of Amazon settlement data into export invoice table...");
// get list of unprocessed settlement reports to export
var settlementList = GetListOfUnprocessedSettlementReports();
if (settlementList == null)
return WithUnitOfWork(uow =>
{
return false;
}
// get list of unprocessed settlement reports to export
var settlementList = GetUnprocessedSettlementReports(uow, true);
if (settlementList == null)
{
return false;
}
// create list of settlement ids for later use
var settlementIdList = new List<string>();
foreach (var settlement in settlementList)
{
settlementIdList.Add(settlement.SettlementId);
}
// create list of settlement ids for later use
var settlementIdList = new List<string>();
foreach (var settlement in settlementList)
{
settlementIdList.Add(settlement.SettlementId);
}
// create list of invoices from settlement reports
var invoiceList = CreateInvoices(settlementList, convertToGbp);
if (invoiceList == null || invoiceList.Any() == false)
{
return false;
}
// create list of invoices from settlement reports
var invoiceList = CreateInvoices(settlementList, convertToGbp);
if (invoiceList == null || invoiceList.Any() == false)
{
return false;
}
// add invoice to export queue and set settlements as processed
Console.Write("\rWriting to database... ");
using (UnitOfWork unitOfWork = new UnitOfWork())
{
// add invoice to export queue and set settlements as processed
Console.Write("\rWriting to database... ");
try
{
var queueService = new Logic.Export.AccountInvoice.QueueService(unitOfWork);
var queueService = new Logic.Export.AccountInvoice.QueueService(uow);
// add temp invoice numbers
queueService.AddTempInvoiceNumber(invoiceList, true);
@@ -89,12 +84,15 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
if (queueService.ErrorMessageIsSet == false && queueInsertResult.Count() == invoiceList.Count())
{
// set settlements to isprocessed
unitOfWork.ImportAmazonRepository.SetAmazonSettlementIsProcessed(settlementIdList, true);
unitOfWork.Commit();
uow.AmazonSettlementRepository.UpdateAmazonSettlementIsProcessed(settlementIdList, true);
uow.Commit();
Console.Write("\r");
_log.LogInformation("\rFinished processing of Amazon settlement data. " + invoiceList.Count() + " invoices created from " + settlementIdList.Count() + " Amazon settlement reports.");
return true;
}
else
{
unitOfWork.Rollback();
uow.Rollback();
string error = queueService.ErrorMessage;
ErrorMessage = "Cancelled processing of Amazon settlement data into export invoice table. " + error;
_log.LogInformation("Cancelled processing of Amazon settlement data into export invoice table...");
@@ -109,115 +107,115 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
_log.LogInformation("Cancelled processing of Amazon settlement data into export invoice table...");
return false;
}
}
Console.Write("\r");
_log.LogInformation("\rFinished processing of Amazon settlement data. " + invoiceList.Count() + " invoices created from " + settlementIdList.Count() + " Amazon settlement reports.");
return true;
});
}
/// <summary>
/// Retrives a list of unprocessed settlement reports from the database, checks for gaps in settlement periods, and validates
/// the settlement data.
/// </summary>
/// <param name="newMarketplaceNameList">Import will fail on new marketplace, add here to bypass this check</param>
/// <param name="updateNullMarketplaceByCurrency">Insert 'Amazon.co.uk' if the market place name is missing and the currecy is GBP</param>
/// <returns></returns>
private List<Model.Import.AmazonSettlement> GetListOfUnprocessedSettlementReports()
private List<Model.Import.AmazonSettlement> GetUnprocessedSettlementReports(IUnitOfWork uow, bool updateNullMarketplaceByCurrency = true)
{
List<Model.Import.AmazonSettlement> settlementList = null;
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
// get list of unprocessed settlement reports to export
settlementList = uow.AmazonSettlementRepository.ReadAmazonSettlements(null, null, false).Values.ToList();
if (settlementList.Any() == false)
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
ErrorMessage = "No new settlement reports to process";
return null;
}
// get list of unprocssed settlement reports to export
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
// test marketplace-name has been sucsessfully entered into settlement table --
// as this is not supplied in the original report from Amazon and has to be inferred from settlement line data or added below
// via currency (null marketplace anme is not picked up in validate stage as null value is valid)
foreach (var settlement in settlementList)
{
settlementList = currentUow.ImportAmazonRepository.ReadAmazonSettlements(null, null, false).Values.ToList();
if (settlementList.Any() == false)
if (settlement.MarketPlaceNameIsSet == false && updateNullMarketplaceByCurrency && settlement.CurrencyCode == CurrencyCode.GBP.ToString())
{
ErrorMessage = "No new settlement reports to process";
// update database with market place name
uow.AmazonSettlementRepository.UpdateAmazonSettlementMarketPlaceName(
settlement.SettlementId
, MarketPlaceEnum.AmazonUK
);
// add to settlement
settlement.MarketPlace = MarketPlaceEnum.AmazonUK;
}
else if (settlement.MarketPlaceNameIsSet == false)
{
string error = "User action required: Enter market place name for amazon settlelment report id:" + settlement.SettlementId + ".";
ErrorMessage = "Cancelled processing of Amazon settlement data into export invoice table: " + error;
_log.LogError(
error
, "Unable to process settlement data from report '" + settlement.SettlementId +
"'. Report header table requires a market place name, which is not supplied in original " +
"report from Amazon. This is useually inferred from settlement lines. " +
"However, in this case, it was not not possible. Manual edit/entry for database table required."
);
_log.LogInformation("Cancelled processing of Amazon settlement data into export invoice table...");
return null;
}
}
// test marketplace-name has been sucsessfully entered into settlement table --
// as this is not supplied in the original report from Amazon and has to be inferred from settlement line data
// this is not picked up in validate stage as null value is valid
foreach (var settlement in settlementList)
// check for time gaps between settlement periods
settlementList = settlementList.OrderBy(x => x.MarketPlace).ThenBy(x => x.StartDate).ToList();
for (var i = 0; i < settlementList.Count; i++)
{
// first marketplace of type in list? retrive the previously completed settlement for that marketplace to compare datetimes
if (i == 0 || settlementList[i].MarketPlace != settlementList[i - 1].MarketPlace)
{
if (settlement.MarketPlaceNameIsSet == false)
// get previously completed settlement for this marketplace
var completedSettlement = uow.AmazonSettlementRepository.ReadAmazonSettlements(
null, new List<string> { settlementList[i].MarketPlace.GetMarketplaceUrl() }, true, true, 1);
if (completedSettlement.Any())
{
string error = "User action required: Enter market place name for amazon settlelment report id:" + settlement.SettlementId + ".";
ErrorMessage = "Cancelled processing of Amazon settlement data into export invoice table: " + error;
_log.LogError(
error
, "Unable to process settlement data from report '" + settlement.SettlementId +
"'. Report header table requires a market place name, which is not supplied in original " +
"report from Amazon. This is useually inferred from settlement lines. " +
"However, in this case, it was not not possible. Manual edit/entry for database table required."
);
_log.LogInformation("Cancelled processing of Amazon settlement data into export invoice table...");
return null;
}
}
// check for time gaps between settlement periods
settlementList = settlementList.OrderBy(x => x.MarketPlace).ThenBy(x => x.StartDate).ToList();
for (var i = 0; i < settlementList.Count; i++)
{
// first marketplace of type in list? retrive the previously completed settlement for that marketplace to compare datetimes
if (i == 0 || settlementList[i].MarketPlace != settlementList[i - 1].MarketPlace)
{
// get previously completed settlement for this marketplace
var completedSettlement = currentUow.ImportAmazonRepository.ReadAmazonSettlements(
null, new List<string> { settlementList[i].MarketPlace.GetMarketplaceUrl() }, true, true, 1);
if (completedSettlement.Any())
{
if (completedSettlement.FirstOrDefault().Value.EndDate != settlementList[i].StartDate)
{
string error = (settlementList[i].StartDate - settlementList[i - 1].EndDate).Days + " day gap in "
+ settlementList[i].MarketPlace.GetMarketplaceUrl() + " settlement data (" + settlementList[i - 1].EndDate.ToString("dd MMM yyyy")
+ " to " + settlementList[i].StartDate.ToString("dd MMM yyyy") + "). Ensure all settlement reports have been imported.";
ErrorMessage = error;
_log.LogError("Cancelled processing of Amazon settlement data into invoice export queue: " + error);
return null;
}
}
else
{
// first settlement for this marketplace, no previous settlement to compare against
// continue on
}
}
else
{
if (settlementList[i - 1].EndDate != settlementList[i].StartDate)
if (completedSettlement.FirstOrDefault().Value.EndDate != settlementList[i].StartDate)
{
string error = (settlementList[i].StartDate - settlementList[i - 1].EndDate).Days + " day gap in "
+ settlementList[i].MarketPlace + " settlement data (" + settlementList[i - 1].EndDate.ToString("dd MMM yyyy")
+ settlementList[i].MarketPlace.GetMarketplaceUrl() + " settlement data (" + settlementList[i - 1].EndDate.ToString("dd MMM yyyy")
+ " to " + settlementList[i].StartDate.ToString("dd MMM yyyy") + "). Ensure all settlement reports have been imported.";
ErrorMessage = error;
_log.LogError("Cancelled processing of Amazon settlement data into invoice export queue: " + error);
return null;
}
}
else
{
// first settlement for this marketplace, no previous settlement to compare against
// continue on
}
}
else
{
if (settlementList[i - 1].EndDate != settlementList[i].StartDate)
{
string error = (settlementList[i].StartDate - settlementList[i - 1].EndDate).Days + " day gap in "
+ settlementList[i].MarketPlace + " settlement data (" + settlementList[i - 1].EndDate.ToString("dd MMM yyyy")
+ " to " + settlementList[i].StartDate.ToString("dd MMM yyyy") + "). Ensure all settlement reports have been imported.";
ErrorMessage = error;
_log.LogError("Cancelled processing of Amazon settlement data into invoice export queue: " + error);
return null;
}
}
}
// validate settlelments
if (settlementList == null || settlementList.Any() == false)
{
_log.LogInformation("No new settlement reports to process.");
return null;
}
// validate settlements
var validate = new Logic.Validate.AmazonSettlement();
foreach (var settlement in settlementList)
{
if (!validate.IsValid(settlement))
{
_log.LogError("Error procesing Amazon Settlement data for export.", validate.ValidationResultListToString());
_log.LogError("Error processing Amazon Settlement data for export.", validate.ValidationResultListToString());
}
}
if (validate.IsValidResult == false)

View File

@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace bnhtrade.Core.Logic.Export.AccountInvoice
{
internal class InvoiceService
{
}
}

View File

@@ -15,22 +15,17 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
/// <summary>
/// Processes the Export Invoice table and exports to Xero
/// </summary>
public class QueueService
public class QueueService : UnitOfWorkBase
{
private Log.LogEvent _log = new Log.LogEvent();
private IEnumerable<int> _exportSaleInvoiceIdList = new List<int>();
private readonly IUnitOfWork _providedUnitOfWork = null;
private readonly bool _ownsUnitOfWork = false;
private UnitOfWork _exportUow = null;
public QueueService()
{
_ownsUnitOfWork = true;
}
internal QueueService(IUnitOfWork unitOfWork)
internal QueueService(IUnitOfWork unitOfWork) : base(unitOfWork)
{
_providedUnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
_ownsUnitOfWork = false;
}
public string ErrorMessage { get; private set; } = null;
@@ -64,26 +59,11 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
{
Init();
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
}
string result = null;
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
return WithUnitOfWork(uow =>
{
result = "_tmp" + currentUow.SequenceGenerator.GetNext("ExportTempInvoiceNumber").ToString("00000000");
if (_ownsUnitOfWork)
{
currentUow.Commit();
}
}
return result;
return "_tmp" + uow.SequenceGenerator.GetNext("ExportTempInvoiceNumber").ToString("00000000");
});
}
public void AddTempInvoiceNumber(IEnumerable<Model.Account.IInvoice> invoiceList, bool overwriteExisting)
@@ -110,30 +90,26 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
{
Init();
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
return WithUnitOfWork(uow =>
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
}
using (_ownsUnitOfWork ? currentUow : null)
{
var returnList = currentUow.ExportInvoiceRepository.GetSalesInvoiceById(invoiceIdList);
var validate = new Logic.Validate.Invoice();
bool isValid = validate.IsValidExportInvoice(returnList.Values.ToList());
return ReadInvoiceById(uow, invoiceIdList);
});
}
if (isValid == false)
{
ErrorMessage = "Reading invoices from database failed validation. See logs for further details.";
_log.LogError("ErrorMessage", validate.ValidationResultListToString());
throw new Exception(ErrorMessage);
}
private Dictionary<int, Model.Account.SalesInvoice> ReadInvoiceById(IUnitOfWork uow, IEnumerable<int> invoiceIdList)
{
var returnList = uow.ExportInvoiceRepository.GetSalesInvoiceById(invoiceIdList);
var validate = new Logic.Validate.Invoice();
bool isValid = validate.IsValidExportInvoice(returnList.Values.ToList());
return returnList;
if (isValid == false)
{
ErrorMessage = "Reading invoices from database failed validation. See logs for further details.";
_log.LogError("ErrorMessage", validate.ValidationResultListToString());
throw new Exception(ErrorMessage);
}
return returnList;
}
/// <summary>
@@ -158,25 +134,12 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
validateInvoice = null;
// save to database
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
return WithUnitOfWork(uow =>
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
}
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
{
var result = currentUow.ExportInvoiceRepository.InsertSalesInvoices(invoiceList);
if (_ownsUnitOfWork)
{
currentUow.Commit();
}
var result = uow.ExportInvoiceRepository.InsertSalesInvoices(invoiceList);
CommitIfOwned(uow);
return result;
}
});
}
/// <summary>
@@ -187,25 +150,10 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
{
Init();
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
return WithUnitOfWork(uow =>
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
}
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
{
var result = currentUow.ExportInvoiceRepository.GetNewInvoiceNumbers(invoiceType).Count();
if (_ownsUnitOfWork)
{
currentUow.Commit();
}
return result;
}
return uow.ExportInvoiceRepository.GetNewInvoiceNumbers(invoiceType).Count();
});
}
/// <summary>
@@ -213,80 +161,45 @@ namespace bnhtrade.Core.Logic.Export.AccountInvoice
/// </summary>
/// <param name="filePath"></param>
/// <param name="firstInvoiceNumber"></param>
public void ExportSalesInvoice(string filePath, int firstInvoiceNumber)
public bool ExportSalesInvoice(string filePath, int firstInvoiceNumber, Func<bool> getUserConfirmation)
{
Init();
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
}
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
return WithUnitOfWork(uow =>
{
var invoiceType = Model.Account.InvoiceType.Sale;
var idList = currentUow.ExportInvoiceRepository.GetNewInvoiceNumbers(invoiceType);
_exportSaleInvoiceIdList = idList.Keys.ToList();
var invoiceList = ReadInvoiceById(idList.Keys.ToList());
// get list of unprocessed invoices
var newInvoiceIdDict = uow.ExportInvoiceRepository.GetNewInvoiceNumbers(invoiceType);
// update db entries with invoice numbers and set to iscompleted=true
foreach (var newInvoiceId in newInvoiceIdDict)
{
string invoiceNumber = "INV-" + firstInvoiceNumber.ToString("000000");
uow.ExportInvoiceRepository.UpdateInvoiceHeaderDetail(newInvoiceId.Key, invoiceNumber, true);
firstInvoiceNumber++;
}
// read invoices from database
var invoiceList = ReadInvoiceById(uow, newInvoiceIdDict.Keys.ToList());
var exportToFile = new Data.Xero.SalesInvoice();
exportToFile.ExportToCsv(invoiceList.Values.ToList(), firstInvoiceNumber, filePath);
}
}
exportToFile.ExportToCsv(invoiceList.Values.ToList(), filePath);
/// <summary>
/// Call this after ExportSalesInvoice() to mark exported invoices as complete
/// </summary>
/// <returns>number of invoices effected</returns>
public int? ExportSalesInvoiceIsComplete()
{
Init();
// get user confitmation before marking as exported
bool userInput = getUserConfirmation.Invoke();
if (_exportSaleInvoiceIdList.Any() == false)
{
ErrorMessage = "Nothing to set as complete, did you call the ExportSalesInvoice method first?";
return null;
}
var parameters = new Dictionary<int, bool>();
foreach (var id in _exportSaleInvoiceIdList)
{
parameters.Add(id, true);
}
// update database
IUnitOfWork currentUow = null;
if (_ownsUnitOfWork)
{
currentUow = new UnitOfWork();
}
else
{
currentUow = _providedUnitOfWork;
}
using (currentUow != null && _ownsUnitOfWork ? currentUow : null)
{
int count = currentUow.ExportInvoiceRepository.SetInvoiceIsCompleteValue(parameters);
if (_exportSaleInvoiceIdList.Count() == count)
if (userInput == false)
{
currentUow.Commit();
return count;
RollbackIfOwned(uow);
ErrorMessage = "User cancelled export, invoices not marked as exported.";
_log.LogInformation(ErrorMessage);
return false;
}
else
{
currentUow.Rollback();
ErrorMessage = "ExportSalesInvoiceIsComplete() Incorrect number of rows updated, changes rolled back.";
_log.LogError(ErrorMessage);
throw new Exception(ErrorMessage);
}
}
CommitIfOwned(uow);
return true;
});
}
}
}