From 76cb8adbe6eae56b2d23696698892fa52a75e222 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 10 Jun 2026 18:09:33 +0200 Subject: [PATCH] =?UTF-8?q?Add=20EK=C3=81ER=20XML=20generation,=20validati?= =?UTF-8?q?on,=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added GenerateEkaerXmlDocument to service and interface for EKÁER tradeCard XML generation and validation from ShippingDocument. - Extended IShippingToEkaerMapper to support document-level mapping. - Updated controller and SignalR client/interfaces with new methods and tags for EKÁER XML and EkaerHistory creation. - Formatted Created/Modified columns in Blazor grids. - Added tests for EkaerHistory creation, XML generation, and idempotency. - Improved null-safety and argument validation in mapping logic. --- .../Services/Ekaer/FruitBankEkaerService.cs | 28 +++- .../Services/Ekaer/IFruitBankEkaerService.cs | 10 ++ .../IFruitBankDataControllerCommon.cs | 2 + .../Services/Ekaer/IShippingToEkaerMapper.cs | 7 + .../Services/Ekaer/ShippingToEkaerMapper.cs | 16 ++- FruitBank.Common/SignalRs/SignalRTags.cs | 2 + .../FruitBankEkaerTests.cs | 123 ++++++++++++++++++ .../Grids/Cargos/GridCargoPartner.razor | 4 +- .../Grids/Cargos/GridCargoTruck.razor | 4 +- .../Grids/Ekaers/GridEkaerHistory.razor | 4 +- .../Grids/Partners/GridPartner.razor | 4 +- .../Grids/Partners/GridPartnerDepot.razor | 4 +- .../Grids/Shippings/GridShipping.razor | 4 +- .../SignalRs/FruitBankSignalRClient.cs | 2 + 14 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index 616cb277..c5384fb4 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -1,3 +1,4 @@ +using AyCode.Services.Nav; using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer.Models; using FruitBank.Common.Entities; @@ -15,12 +16,14 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService { private readonly IShippingToEkaerMapper _mapper; private readonly IEkaerSubmitService _submitService; + private readonly IEkaerTradeCardValidator _validator; private readonly EkaerCompanyInfo _company; - public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, EkaerCompanyInfo company) + public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerCompanyInfo company) { _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _submitService = submitService ?? throw new ArgumentNullException(nameof(submitService)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); _company = company ?? throw new ArgumentNullException(nameof(company)); } @@ -32,4 +35,27 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService var operations = _mapper.MapShipping(shipping, _company, operation); return _submitService.SubmitAsync(operations, cancellationToken); } + + public EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null) + { + ArgumentNullException.ThrowIfNull(document); + + ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false }; + + var operation = new TradeCardOperationType + { + Index = 1, + Operation = OperationType.Create, + TradeCard = _mapper.MapDocument(document, _company), + }; + + var errors = _validator.Validate(operation); + + // Az XML validációs hibánál IS tárolódik — a detail-nézetben így látszik, mi hiányzik. + ekaerHistory.XmlDoc = NavXmlHelper.Serialize(operation.TradeCard); + ekaerHistory.Status = errors.Count == 0 ? EkaerStatus.Generated : EkaerStatus.ValidationError; + ekaerHistory.ErrorText = errors.Count == 0 ? null : string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage)); + + return ekaerHistory; + } } diff --git a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs index d9f13b9c..244bbf3a 100644 --- a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs @@ -16,4 +16,14 @@ public interface IFruitBankEkaerService /// vagy a NAV-válasz — lásd . /// Task SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default); + + /// + /// Egy szállítólevélből legenerálja az EKÁER tradeCard XML-t és validálja — a (meglévő vagy új) + /// rekordot tölti: XmlDoc + Status + /// ( / ) + ErrorText. + /// NEM perzisztál és NEM hív NAV-ot — a mentés (upsert) a hívó SignalR endpoint dolga. + /// + /// A szállítólevél a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek). + /// A dokumentum meglévő rekordja (újrageneráláskor); null → új rekord készül. + EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null); } diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index 30c81db2..9e31b37c 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -34,6 +34,8 @@ public interface IFruitBankDataControllerCommon public Task?> GetEkaerHistoriesByForeignKey(int foreignKey); public Task AddEkaerHistory(EkaerHistory ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory); + public Task GenerateEkaerXmlDocument(int shippingDocumentId); + public Task CreateEkaerHistory(int foreignKey, bool isOutgoing); #endregion EkaerHistory #region CargoPartner diff --git a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs index a3430daf..31b2b70b 100644 --- a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs @@ -22,4 +22,11 @@ public interface IShippingToEkaerMapper /// A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely. /// A tradeCard művelet típusa. Alapértelmezés: . IReadOnlyList MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create); + + /// + /// EGY szállítólevelet () képez le egy tradeCard-dá — a dokumentum-szintű + /// EKÁER-granularitás egysége (1 dokumentum = 1 tradeCard = 1 TCN). A jármű/fuvarozó adatok a + /// document.Shipping-ből jönnek; ha az nincs betöltve, ezek üresen maradnak (a validátor jelzi). + /// + TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company); } diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs index c038c745..7b221ff3 100644 --- a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs @@ -42,7 +42,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper return operations; } - private static TradeCardType BuildTradeCard(Shipping shipping, ShippingDocument document, EkaerCompanyInfo company) + public TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(company); + + return BuildTradeCard(document.Shipping, document, company); + } + + private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company) { var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase @@ -64,7 +72,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper DestinationAddress = Truncate(company.FullAddress, 200), // Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név. - CarrierText = shipping.CargoPartner?.Name, + CarrierText = shipping?.CargoPartner?.Name, // Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye. UnloadLocation = company.UnloadLocation, @@ -72,8 +80,8 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper }; // Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2). - if (shipping.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); - if (shipping.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer); + if (shipping?.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); + if (shipping?.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer); foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item)); return tradeCard; diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs index 3c23bee4..77e72728 100644 --- a/FruitBank.Common/SignalRs/SignalRTags.cs +++ b/FruitBank.Common/SignalRs/SignalRTags.cs @@ -123,6 +123,8 @@ public class SignalRTags : AcSignalRTags public const int GetEkaerHistoriesByForeignKey = 187; public const int AddEkaerHistory = 188; public const int UpdateEkaerHistory = 189; + public const int GenerateEkaerXmlDocument = 190; + public const int CreateEkaerHistory = 191; public const int AuthenticateUser = 195; public const int RefreshToken = 200; diff --git a/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs new file mode 100644 index 00000000..12bbe5e9 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs @@ -0,0 +1,123 @@ +using AyCode.Services.Nav; +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common; +using FruitBank.Common.Entities; +using FruitBankHybrid.Shared.Services.SignalRs; + +namespace FruitBankHybrid.Shared.Tests +{ + [TestClass] + public sealed class FruitBankEkaerTests + { + private FruitBankSignalRClient _signalRClient = null!; + + [TestInitialize] + public void TestInit() + { + if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests)); + } + + #region EkaerHistory / Create + + /// + /// Backfill: minden meglévő szállítólevélre létrehozza az EkaerHistory rekordot (Pending, XML nélkül). + /// A generálás a grid Generate gombjával, kézzel történik. Idempotens: újrafuttatva nem duplikál. + /// + [TestMethod] + public async Task CreateEkaerHistoryForAllShippingDocumentsTest() + { + var shippingDocuments = await _signalRClient.GetShippingDocuments(); + + Assert.IsNotNull(shippingDocuments); + Assert.IsNotEmpty(shippingDocuments); + + foreach (var shippingDocument in shippingDocuments) + { + var ekaerHistory = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false); + + Console.WriteLine($"doc#{shippingDocument.Id}: EkaerHistory Id: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Id.ToString())}; Status: {ekaerHistory?.Status}"); + + Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}"); + Assert.IsGreaterThan(0, ekaerHistory.Id, $"shippingDocument.Id: {shippingDocument.Id}"); + Assert.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey); + Assert.IsFalse(ekaerHistory.IsOutgoing); + } + + // Idempotencia: a második hívás a meglévőt adja vissza, nem duplikál. + var firstDocumentId = shippingDocuments[0].Id; + var again = await _signalRClient.CreateEkaerHistory(firstDocumentId, false); + var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(firstDocumentId); + + Assert.IsNotNull(again); + Assert.IsNotNull(histories); + Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {firstDocumentId}"); + } + + #endregion EkaerHistory / Create + + #region EkaerHistory / Generate + + /// + /// Backfill + teljes Generate-út teszt: minden meglévő szállítólevélre legenerálja az EKÁER XML-t + /// (rekord upsert a szerveren), így a grid valós adatot kap és a Generate gomb útvonala tesztelt. + /// + //[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog. + public async Task GenerateEkaerXmlDocumentForAllShippingDocumentsTest() + { + var shippingDocuments = await _signalRClient.GetShippingDocuments(); + + Assert.IsNotNull(shippingDocuments); + Assert.IsNotEmpty(shippingDocuments); + + foreach (var shippingDocument in shippingDocuments) + { + var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(shippingDocument.Id); + + // A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon. + Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}"); + if (!string.IsNullOrWhiteSpace(ekaerHistory?.ErrorText)) Console.WriteLine($" ErrorText: {ekaerHistory.ErrorText}"); + + Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}"); + Assert.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey); + Assert.IsFalse(ekaerHistory.IsOutgoing); + Assert.IsFalse(string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc), $"XmlDoc üres; shippingDocument.Id: {shippingDocument.Id}"); + Assert.IsTrue(ekaerHistory.Status is EkaerStatus.Generated or EkaerStatus.ValidationError, + $"Status: {ekaerHistory.Status}; shippingDocument.Id: {shippingDocument.Id}; ErrorText: {ekaerHistory.ErrorText}"); + + // A grid útvonala: az XmlDoc visszaolvasható tradeCard-dá. + var tradeCard = NavXmlHelper.Deserialize(ekaerHistory.XmlDoc!); + + Assert.IsNotNull(tradeCard); + Console.WriteLine($" items: {tradeCard.Items.Count}"); + } + } + + /// Idempotencia: az újragenerálás NEM duplikál — dokumentumonként egy bejövő rekord marad. + //[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog. + public async Task GenerateEkaerXmlDocumentIsIdempotentTest() + { + var shippingDocuments = await _signalRClient.GetShippingDocuments(); + + Assert.IsNotNull(shippingDocuments); + Assert.IsNotEmpty(shippingDocuments); + + var shippingDocumentId = shippingDocuments[0].Id; + + var first = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId); + var second = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId); + + Assert.IsNotNull(first); + Assert.IsNotNull(second); + Assert.AreEqual(first.Id, second.Id, "Az újragenerálás új rekordot hozott létre frissítés helyett!"); + + var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(shippingDocumentId); + + Assert.IsNotNull(histories); + Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {shippingDocumentId}"); + } + + #endregion EkaerHistory / Generate + } +} diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor index 07ea7f23..626bcf65 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor @@ -38,8 +38,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor index 3663e202..61144166 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor @@ -32,8 +32,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index ed881786..be85e3d2 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -29,8 +29,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor index 61947300..c4e9a301 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor @@ -38,8 +38,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor index 26a090f1..0862fd24 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor @@ -35,8 +35,8 @@ - - + + @* diff --git a/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor b/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor index c668cd3a..5a9a2378 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor @@ -86,8 +86,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 1f10aa12..8dbe9808 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -79,6 +79,8 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task?> GetEkaerHistoriesByForeignKey(int foreignKey) => GetAllAsync>(SignalRTags.GetEkaerHistoriesByForeignKey, [foreignKey]); public Task AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory); + public Task GenerateEkaerXmlDocument(int shippingDocumentId) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, shippingDocumentId); + public Task CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]); #endregion EkaerHistory #region CargoPartner