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);
+}