From 3db26fbfa3eb5f6d13ae10a8749d17c62e3dc5c5 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 2 Jun 2026 15:23:42 +0200 Subject: [PATCH] =?UTF-8?q?Add=20EK=C3=81ER=20submit/validation=20layer=20?= =?UTF-8?q?with=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced IEkaerSubmitService and EkaerSubmitService for orchestrated EKÁER trade card validation and submission, with result encapsulation via EkaerSubmitResult. Added IEkaerTradeCardValidator and EkaerTradeCardValidator for schema and business rule validation. Comprehensive unit tests cover validation logic and submission flow. All code is generic and project-agnostic. --- .../Nav/EkaerSubmitServiceTests.cs | 122 ++++++++++++ .../Nav/EkaerTradeCardValidatorTests.cs | 188 ++++++++++++++++++ .../Nav/Ekaer/EkaerSubmitResult.cs | 33 +++ .../Nav/Ekaer/EkaerSubmitService.cs | 45 +++++ .../Nav/Ekaer/EkaerTradeCardValidator.cs | 131 ++++++++++++ .../Nav/Ekaer/IEkaerSubmitService.cs | 17 ++ .../Nav/Ekaer/IEkaerTradeCardValidator.cs | 23 +++ 7 files changed, 559 insertions(+) create mode 100644 AyCode.Services.Tests/Nav/EkaerSubmitServiceTests.cs create mode 100644 AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs create mode 100644 AyCode.Services/Nav/Ekaer/EkaerSubmitResult.cs create mode 100644 AyCode.Services/Nav/Ekaer/EkaerSubmitService.cs create mode 100644 AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs create mode 100644 AyCode.Services/Nav/Ekaer/IEkaerSubmitService.cs create mode 100644 AyCode.Services/Nav/Ekaer/IEkaerTradeCardValidator.cs diff --git a/AyCode.Services.Tests/Nav/EkaerSubmitServiceTests.cs b/AyCode.Services.Tests/Nav/EkaerSubmitServiceTests.cs new file mode 100644 index 0000000..d3ded02 --- /dev/null +++ b/AyCode.Services.Tests/Nav/EkaerSubmitServiceTests.cs @@ -0,0 +1,122 @@ +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Text; +using AyCode.Services.Nav; +using AyCode.Services.Nav.Ekaer; +using AyCode.Services.Nav.Ekaer.Models; +using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType; + +namespace AyCode.Services.Tests.Nav; + +/// +/// Az tesztjei: a validate → send sorrend a valódi +/// fölött, fake -rel (nincs valódi hálózat) és stub validátorral. +/// +[TestClass] +public sealed class EkaerSubmitServiceTests +{ + // ---- Test doubles ------------------------------------------------------- + + private sealed class FakeHttpMessageHandler(HttpStatusCode status, string body) : HttpMessageHandler + { + public int CallCount { get; private set; } + public string? LastRequestBody { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + CallCount++; + if (request.Content is not null) + LastRequestBody = await request.Content.ReadAsStringAsync(cancellationToken); + return new HttpResponseMessage(status) { Content = new StringContent(body, Encoding.UTF8, "text/xml") }; + } + } + + private sealed class StubValidator(IReadOnlyList errors) : IEkaerTradeCardValidator + { + public IReadOnlyList Validate(TradeCardOperationType operation) => errors; + public IReadOnlyList Validate(IEnumerable operations) => errors; + } + + private sealed class FakeCredentials : INavCredentials + { + public string User => "u"; + public string Password => "p"; + public string SigningKey => "k"; + public string TaxNumber => "12345678"; + public string BaseUrl => "https://test.example"; + } + + // ---- Helpers ------------------------------------------------------------ + + private static EkaerSubmitService CreateSut(IReadOnlyList validationErrors, FakeHttpMessageHandler handler) + { + var manageService = new EkaerManageService(new HttpClient(handler), new FakeCredentials()); + return new EkaerSubmitService(manageService, new StubValidator(validationErrors)); + } + + private static string OkResponseXml() + => NavXmlHelper.Serialize(new ManageTradeCardsResponse { Result = new BaseResultType { FuncCode = FunctionCodeType.Ok } }); + + private static TradeCardOperationType SomeOperation() + => new() { Index = 1, Operation = OperationType.Create, TradeCard = new TradeCardType { TradeType = TradeType.I } }; + + // ---- Tesztek ------------------------------------------------------------ + + [TestMethod] + public async Task SubmitAsync_ValidationFails_DoesNotSend() + { + var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, OkResponseXml()); + var sut = CreateSut([new ValidationResult("teszthiba")], handler); + + var result = await sut.SubmitAsync([SomeOperation()]); + + Assert.IsFalse(result.IsValid); + Assert.AreEqual(1, result.ValidationErrors.Count); + Assert.IsNull(result.Response); + Assert.AreEqual(0, handler.CallCount, "validációs hiba esetén NEM mehet ki kérés a NAV-nak"); + } + + [TestMethod] + public async Task SubmitAsync_Valid_SendsAndReturnsResponse() + { + var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, OkResponseXml()); + var sut = CreateSut([], handler); + + var result = await sut.SubmitAsync([SomeOperation()]); + + Assert.IsTrue(result.IsValid); + Assert.AreEqual(1, handler.CallCount, "érvényes esetben egy kérés megy ki"); + Assert.IsNotNull(result.Response); + Assert.AreEqual(0, result.ValidationErrors.Count); + } + + [TestMethod] + public async Task SubmitAsync_Valid_RequestContainsOperations() + { + var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, OkResponseXml()); + var sut = CreateSut([], handler); + + await sut.SubmitAsync([SomeOperation()]); + + Assert.IsNotNull(handler.LastRequestBody); + StringAssert.Contains(handler.LastRequestBody, "tradeCardOperation", "a beküldött XML tartalmazza a műveleteket"); + } + + [TestMethod] + public async Task SubmitAsync_NavHttpError_PropagatesNavReportException() + { + var handler = new FakeHttpMessageHandler(HttpStatusCode.InternalServerError, "boom"); + var sut = CreateSut([], handler); + + await Assert.ThrowsExactlyAsync(() => sut.SubmitAsync([SomeOperation()])); + } + + [TestMethod] + public async Task SubmitAsync_NullOperations_Throws() + { + var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, OkResponseXml()); + var sut = CreateSut([], handler); + + await Assert.ThrowsExactlyAsync(() => sut.SubmitAsync(null!)); + } +} diff --git a/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs b/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs new file mode 100644 index 0000000..0784d9e --- /dev/null +++ b/AyCode.Services.Tests/Nav/EkaerTradeCardValidatorTests.cs @@ -0,0 +1,188 @@ +using System.ComponentModel.DataAnnotations; +using AyCode.Services.Nav.Ekaer; +using AyCode.Services.Nav.Ekaer.Models; +using TradeReasonType = AyCode.Services.Nav.Ekaer.Models.Common.TradeReasonType; +using TradeType = AyCode.Services.Nav.Ekaer.Models.Common.TradeType; + +namespace AyCode.Services.Tests.Nav; + +/// +/// Unit tesztek az -ra: séma-szintű (generált DataAnnotations) és +/// üzleti szabályok (vonó jármű, tétel, feladó/címzett) ellenőrzése, hibalista-jelleggel. A validátor +/// általános NAV/EKÁER réteg — a teszt csak az AyCode.Services-re támaszkodik (semmi projekt-specifikus). +/// +[TestClass] +public sealed class EkaerTradeCardValidatorTests +{ + private static readonly EkaerTradeCardValidator Sut = new(); + + // ---- Helpers ------------------------------------------------------------ + + /// Teljesen érvényes Create művelet, egy érvényes tétellel. A tesztek ezt „rontják el". + private static TradeCardOperationType ValidOperation() + { + var operation = new TradeCardOperationType + { + Index = 1, + Operation = OperationType.Create, + TradeCard = new TradeCardType + { + TradeType = TradeType.I, + SellerName = "Beszállító Kft", + SellerVatNumber = "12345678-2-42", + SellerCountry = "HU", + SellerAddress = "1011 Budapest Fo utca 1", + DestinationName = "FruitBank Kft", + DestinationVatNumber = "98765432-2-41", + DestinationCountry = "HU", + DestinationAddress = "1102 Budapest Raktar utca 5", + Vehicle = new BasicVehicleDetailType { PlateNumber = "ABC123", Country = "HU" }, + }, + }; + operation.TradeCard.Items.Add(new TradeCardItemType + { + ItemExternalId = "1", + TradeReason = TradeReasonType.A, + ProductVtsz = "08081010", + ProductName = "Alma", + Weight = 123.5m, + }); + return operation; + } + + private static bool HasError(IReadOnlyList errors, string fragment) + => errors.Any(e => e.ErrorMessage?.Contains(fragment, StringComparison.OrdinalIgnoreCase) == true); + + // ---- Érvényes eset ------------------------------------------------------ + + [TestMethod] + public void Validate_ValidOperation_NoErrors() + { + var errors = Sut.Validate(ValidOperation()); + Assert.AreEqual(0, errors.Count, $"érvényes művelet nem adhat hibát; kapott: {string.Join(" | ", errors.Select(e => e.ErrorMessage))}"); + } + + // ---- Üzleti szabályok --------------------------------------------------- + + [TestMethod] + public void Validate_MissingVehicle_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Vehicle = null; + Assert.IsTrue(HasError(Sut.Validate(op), "vehicle"), "a hiányzó vonó jármű hibát ad"); + } + + [TestMethod] + public void Validate_NoItems_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items.Clear(); + Assert.IsTrue(HasError(Sut.Validate(op), "tétel"), "a tétel nélküli tradeCard hibát ad"); + } + + [TestMethod] + public void Validate_MissingSellerName_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.SellerName = null; + Assert.IsTrue(HasError(Sut.Validate(op), "feladó"), "hiányzó feladónév → hiba"); + } + + [TestMethod] + public void Validate_MissingDestinationName_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.DestinationName = " "; + Assert.IsTrue(HasError(Sut.Validate(op), "címzett"), "üres/whitespace címzettnév → hiba"); + } + + // ---- Séma-szint (generált DataAnnotations) ------------------------------ + + [TestMethod] + public void Validate_InvalidVtszPattern_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].ProductVtsz = "12"; // pattern [0-9]{4,8} → túl rövid + Assert.IsTrue(HasError(Sut.Validate(op), "productVtsz"), "rossz VTSZ-formátum → séma hiba"); + } + + [TestMethod] + public void Validate_NullVtsz_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].ProductVtsz = null; // a Required kapja el + Assert.IsTrue(HasError(Sut.Validate(op), "productVtsz"), "null VTSZ → Required hiba"); + } + + [TestMethod] + public void Validate_EmptyVtsz_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].ProductVtsz = ""; // a séma átengedné — az üzleti szabály kapja el + Assert.IsTrue(HasError(Sut.Validate(op), "productVtsz"), "üres VTSZ → üzleti szabály hiba"); + } + + [TestMethod] + public void Validate_InvalidPlateNumber_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Vehicle!.PlateNumber = "AB"; // MinLength 4 / pattern sérül + var errors = Sut.Validate(op); + Assert.IsTrue(HasError(errors, "plateNumber") || HasError(errors, "vehicle"), "túl rövid rendszám → séma hiba"); + } + + [TestMethod] + public void Validate_MissingProductName_ReportsError() + { + var op = ValidOperation(); + op.TradeCard!.Items[0].ProductName = null; // [Required(AllowEmptyStrings=false)] + Assert.IsTrue(HasError(Sut.Validate(op), "productName"), "hiányzó terméknév → séma hiba"); + } + + // ---- Lista / művelet-szint ---------------------------------------------- + + [TestMethod] + public void Validate_EmptyList_ReportsError() + { + var errors = Sut.Validate(new List()); + Assert.IsTrue(HasError(errors, "legalább egy"), "üres lista → hiba"); + } + + [TestMethod] + public void Validate_DeleteWithoutTradeCard_DoesNotRequireTradeCard() + { + var op = new TradeCardOperationType { Index = 1, Operation = OperationType.Delete, Tcn = "ABC123" }; + Assert.IsFalse(HasError(Sut.Validate(op), "a tradeCard kötelező"), "delete + Tcn esetén a tradeCard hiánya nem hiba"); + } + + [TestMethod] + public void Validate_NonDeleteWithoutTradeCard_ReportsError() + { + var op = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = null }; + Assert.IsTrue(HasError(Sut.Validate(op), "tradeCard"), "create esetén a tradeCard kötelező"); + } + + // ---- Hibalista-jelleg: minden hibát összegyűjt -------------------------- + + [TestMethod] + public void Validate_CollectsAllErrors_DoesNotStopAtFirst() + { + var op = ValidOperation(); + op.TradeCard!.Vehicle = null; // üzleti + op.TradeCard.Items.Clear(); // üzleti + op.TradeCard.SellerName = null; // üzleti + op.TradeCard.DestinationVatNumber = null; // üzleti + + var errors = Sut.Validate(op); + Assert.IsTrue(errors.Count >= 4, $"minden hibát összegyűjt, nem áll meg az elsőnél; kapott {errors.Count}"); + } + + [TestMethod] + public void Validate_IndexedPath_WhenMultipleOperations() + { + var bad = ValidOperation(); + bad.TradeCard!.Vehicle = null; + var errors = Sut.Validate([ValidOperation(), bad]); + Assert.IsTrue(errors.Any(e => e.ErrorMessage?.Contains("[1]") == true), "a hibák a művelet indexével prefixeltek"); + } +} diff --git a/AyCode.Services/Nav/Ekaer/EkaerSubmitResult.cs b/AyCode.Services/Nav/Ekaer/EkaerSubmitResult.cs new file mode 100644 index 0000000..44737d2 --- /dev/null +++ b/AyCode.Services/Nav/Ekaer/EkaerSubmitResult.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using AyCode.Services.Nav.Ekaer.Models; + +namespace AyCode.Services.Nav.Ekaer; + +/// +/// Egy EKÁER beküldési kísérlet eredménye: vagy validációs hibák (ekkor NEM ment ki kérés a NAV-nak), +/// vagy a NAV-válasz (sikeres beküldés). +/// +public sealed class EkaerSubmitResult +{ + private EkaerSubmitResult(bool isValid, IReadOnlyList validationErrors, ManageTradeCardsResponse? response) + { + IsValid = isValid; + ValidationErrors = validationErrors; + Response = response; + } + + /// Igaz, ha a validáció hibátlan volt (és így a beküldés megtörtént). + public bool IsValid { get; } + + /// A validációs hibák (üres, ha érvényes volt). Ha nem üres, a bejelentés NEM lett elküldve. + public IReadOnlyList ValidationErrors { get; } + + /// A NAV-válasz, ha a beküldés megtörtént; egyébként null. + public ManageTradeCardsResponse? Response { get; } + + /// Validációs hibák miatt elutasított beküldés (nem ment ki kérés a NAV-nak). + public static EkaerSubmitResult Invalid(IReadOnlyList validationErrors) => new(false, validationErrors, null); + + /// Sikeresen elküldött beküldés, a NAV-válasszal. + public static EkaerSubmitResult Sent(ManageTradeCardsResponse response) => new(true, [], response); +} diff --git a/AyCode.Services/Nav/Ekaer/EkaerSubmitService.cs b/AyCode.Services/Nav/Ekaer/EkaerSubmitService.cs new file mode 100644 index 0000000..0b23a30 --- /dev/null +++ b/AyCode.Services/Nav/Ekaer/EkaerSubmitService.cs @@ -0,0 +1,45 @@ +using AyCode.Services.Nav.Ekaer.Models; + +namespace AyCode.Services.Nav.Ekaer; + +/// +/// +/// A NAV-küldést az végzi (auth + HTTP), a validációt az +/// . Ez az osztály csak a sorrendet (validate → send) köti egységbe. +/// +public sealed class EkaerSubmitService : IEkaerSubmitService +{ + private readonly EkaerManageService _manageService; + private readonly IEkaerTradeCardValidator _validator; + + public EkaerSubmitService(EkaerManageService manageService, IEkaerTradeCardValidator validator) + { + _manageService = manageService ?? throw new ArgumentNullException(nameof(manageService)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); + } + + public async Task SubmitAsync(IReadOnlyList operations, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(operations); + + // 1) Validáció — bármilyen hiba esetén NEM küldünk; a teljes hibalistát visszaadjuk. + var errors = _validator.Validate(operations); + if (errors.Count > 0) + return EkaerSubmitResult.Invalid(errors); + + // 2) Request összeállítás. A Header/User példányt itt kell létrehozni (a generált ctor csak a + // TradeCardOperations kollekciót inicializálja); a TARTALMUKAT (requestId, timestamp, hash-ek) az + // ApplyAuthentication tölti a NavReportServiceBase-ben. + var request = new ManageTradeCardsRequest + { + Header = new BasicHeaderType(), + User = new UserHeaderType(), + }; + foreach (var operation in operations) + request.TradeCardOperations.Add(operation); + + // 3) Küldés. NAV-oldali hibánál NavReportException propagál (a hívó logolja/kezeli). + var response = await _manageService.ManageAsync(request, cancellationToken).ConfigureAwait(false); + return EkaerSubmitResult.Sent(response); + } +} diff --git a/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs b/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs new file mode 100644 index 0000000..bbacb86 --- /dev/null +++ b/AyCode.Services/Nav/Ekaer/EkaerTradeCardValidator.cs @@ -0,0 +1,131 @@ +using System.ComponentModel.DataAnnotations; +using AyCode.Services.Nav.Ekaer.Models; + +namespace AyCode.Services.Nav.Ekaer; + +/// +/// +/// Két forrásból gyűjt hibát: +/// +/// a generált modellek -jai (Required / RegularExpression / Min-MaxLength), +/// rekurzívan a tradeCard + tételek + jármű + helyszínek fölött — ezt a végzi; +/// az XSD-ben NEM jelölt, de a NAV által megkövetelt üzleti szabályok (vonó jármű, legalább egy tétel, +/// feladó/címzett alapadatok) — ezeket kézzel ellenőrizzük. +/// +/// +public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator +{ + public IReadOnlyList Validate(TradeCardOperationType operation) + { + ArgumentNullException.ThrowIfNull(operation); + var errors = new List(); + + ValidateOperation(operation, "tradeCardOperation", errors); + return errors; + } + + public IReadOnlyList Validate(IEnumerable operations) + { + ArgumentNullException.ThrowIfNull(operations); + var list = operations.ToList(); + var errors = new List(); + + if (list.Count == 0) + { + errors.Add(Error("tradeCardOperations", "legalább egy tradeCard művelet szükséges")); + return errors; + } + + for (var i = 0; i < list.Count; i++) + ValidateOperation(list[i], $"tradeCardOperations[{i}]", errors); + + return errors; + } + + private static void ValidateOperation(TradeCardOperationType operation, string path, List errors) + { + // A művelet saját attribútumai (index, operation [Required]). + ValidateAnnotations(operation, path, errors); + + var tradeCard = operation.TradeCard; + if (tradeCard is null) + { + // delete művelethez elég a Tcn; minden más művelethez a tradeCard kötelező. + if (operation.Operation != OperationType.Delete) + errors.Add(Error($"{path}.tradeCard", "a tradeCard kötelező (kivéve delete művelet, ahol a Tcn elég)")); + return; + } + + ValidateTradeCard(tradeCard, $"{path}.tradeCard", errors); + } + + private static void ValidateTradeCard(TradeCardType tradeCard, string path, List errors) + { + // 1) Séma-szint: a generált DataAnnotations a tradeCard saját + örökölt property-jein. + ValidateAnnotations(tradeCard, path, errors); + + // 2) Séma-szint a beágyazott objektumokon (a Validator nem rekurzív, ezért kézzel járjuk be). + if (tradeCard.Vehicle is not null) ValidateAnnotations(tradeCard.Vehicle, $"{path}.vehicle", errors); + if (tradeCard.Vehicle2 is not null) ValidateAnnotations(tradeCard.Vehicle2, $"{path}.vehicle2", errors); + if (tradeCard.LoadLocation is not null) ValidateAnnotations(tradeCard.LoadLocation, $"{path}.loadLocation", errors); + if (tradeCard.UnloadLocation is not null) ValidateAnnotations(tradeCard.UnloadLocation, $"{path}.unloadLocation", errors); + + for (var i = 0; i < tradeCard.Items.Count; i++) + { + var itemPath = $"{path}.items[{i}]"; + var item = tradeCard.Items[i]; + + ValidateAnnotations(item, itemPath, errors); + + // A productVtsz a modellen [Required(AllowEmptyStrings=true)] + pattern — az üres string ("") elcsúszna + // a sémán (a null bukik a Required-en, az "" nem, mert a RegularExpression az üreset validnak veszi), + // ezért üzleti szabállyal lezárjuk. A { Length: 0 } property-pattern a null-ra NEM illeszkedik → nincs dupla hiba. + if (item.ProductVtsz is { Length: 0 }) errors.Add(Error($"{itemPath}.productVtsz", "a tétel VTSZ-száma (productVtsz) nem lehet üres")); + } + + // 3) Üzleti szabályok (az XSD nem jelöli [Required]-ként, de a NAV megköveteli). + if (tradeCard.Vehicle is null) errors.Add(Error($"{path}.vehicle", "a vonó jármű (rendszám) kötelező — rendszám nélkül a NAV elutasítja a bejelentést")); + if (tradeCard.Items.Count == 0) errors.Add(Error($"{path}.items", "legalább egy tétel szükséges")); + + RequireText(tradeCard.SellerName, $"{path}.sellerName", "a feladó (eladó) neve kötelező", errors); + RequireText(tradeCard.SellerVatNumber, $"{path}.sellerVatNumber", "a feladó adószáma kötelező", errors); + RequireText(tradeCard.SellerCountry, $"{path}.sellerCountry", "a feladó országkódja kötelező", errors); + RequireText(tradeCard.DestinationName, $"{path}.destinationName", "a címzett neve kötelező", errors); + RequireText(tradeCard.DestinationVatNumber, $"{path}.destinationVatNumber", "a címzett adószáma kötelező", errors); + RequireText(tradeCard.DestinationCountry, $"{path}.destinationCountry", "a címzett országkódja kötelező", errors); + } + + /// + /// A generált modell -jainak ellenőrzése (Required / pattern / hossz), + /// property-szelektíven. A generátor a nullable érték-mezőkhöz *Value + *Specified segéd-property-ket + /// készít, és a tényleges attribútumok ezeken ülnek; a *Value default értéke (pl. a 0001-01-01 DateTime a + /// timestamp-pattern ellen) téves hibát adna, ezért kihagyjuk őket — a serializáció úgyis a *Specified + /// flag alapján dönt a kiírásról, így a default *Value sosem kerül ki a beküldött XML-be. + /// + private static void ValidateAnnotations(object instance, string path, List errors) + { + foreach (var property in instance.GetType().GetProperties()) + { + if (property.GetIndexParameters().Length != 0) continue; // indexer kihagyása + if (property.Name.EndsWith("Value", StringComparison.Ordinal) || + property.Name.EndsWith("Specified", StringComparison.Ordinal)) + continue; + + var context = new ValidationContext(instance) { MemberName = property.Name }; + var local = new List(); + + Validator.TryValidateProperty(property.GetValue(instance), context, local); + + foreach (var result in local) + errors.Add(new ValidationResult($"{path}.{property.Name}: {result.ErrorMessage}", result.MemberNames)); + } + } + + private static void RequireText(string? value, string path, string message, List errors) + { + if (string.IsNullOrWhiteSpace(value)) + errors.Add(Error(path, message)); + } + + private static ValidationResult Error(string path, string message) => new($"{path}: {message}"); +} diff --git a/AyCode.Services/Nav/Ekaer/IEkaerSubmitService.cs b/AyCode.Services/Nav/Ekaer/IEkaerSubmitService.cs new file mode 100644 index 0000000..708ba3c --- /dev/null +++ b/AyCode.Services/Nav/Ekaer/IEkaerSubmitService.cs @@ -0,0 +1,17 @@ +using AyCode.Services.Nav.Ekaer.Models; + +namespace AyCode.Services.Nav.Ekaer; + +/// +/// Általános EKÁER beküldés-orchestráció: validál (a generált tradeCard-okat a NAV-szabályok ellen), és csak +/// hibátlan esetben küld. Nem ismer projekt-specifikus (pl. FruitBank) típust — a már leképzett műveleteket kapja. +/// +public interface IEkaerSubmitService +{ + /// + /// Validálja, majd — ha hibátlan — elküldi a tradeCard műveleteket a NAV-nak. + /// Validációs hiba esetén (nem megy ki kérés); + /// NAV-oldali hiba esetén NavReportException propagál. + /// + Task SubmitAsync(IReadOnlyList operations, CancellationToken cancellationToken = default); +} diff --git a/AyCode.Services/Nav/Ekaer/IEkaerTradeCardValidator.cs b/AyCode.Services/Nav/Ekaer/IEkaerTradeCardValidator.cs new file mode 100644 index 0000000..3db167c --- /dev/null +++ b/AyCode.Services/Nav/Ekaer/IEkaerTradeCardValidator.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using AyCode.Services.Nav.Ekaer.Models; + +namespace AyCode.Services.Nav.Ekaer; + +/// +/// Az EKÁER tradeCard műveletek beküldés-előtti ellenőrzése. A NAV-séma (a generált modellek +/// -jai: Required / pattern / hossz) ÉS az XSD-n felüli üzleti szabályok +/// (pl. a vonó jármű kötelező) ellenőrzése. Minden hibát összegyűjt — nem az elsőnél áll meg. +/// +/// +/// Általános NAV/EKÁER réteg — nem ismer projekt-specifikus (pl. FruitBank) típust, csak a generált modelleket. +/// A hívó (pl. a szerver-oldali EKÁER-service) a beküldés ELŐTT hívja: ha a visszaadott lista nem üres, +/// a bejelentés NEM küldhető. A NAV-validációs szabályok kivonata: Nav/docs/EKAER_VALIDATION.md. +/// +public interface IEkaerTradeCardValidator +{ + /// Egyetlen művelet ellenőrzése. Üres lista = érvényes. + IReadOnlyList Validate(TradeCardOperationType operation); + + /// Több művelet ellenőrzése; a hibák a művelet indexével prefixelve. Üres lista = mind érvényes. + IReadOnlyList Validate(IEnumerable operations); +}