Add EKÁER submit/validation layer with tests
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.
This commit is contained in:
parent
a4cd8f3f0f
commit
3db26fbfa3
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Az <see cref="EkaerSubmitService"/> tesztjei: a validate → send sorrend a valódi <see cref="EkaerManageService"/>
|
||||
/// fölött, fake <see cref="HttpMessageHandler"/>-rel (nincs valódi hálózat) és stub validátorral.
|
||||
/// </summary>
|
||||
[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<HttpResponseMessage> 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<ValidationResult> errors) : IEkaerTradeCardValidator
|
||||
{
|
||||
public IReadOnlyList<ValidationResult> Validate(TradeCardOperationType operation) => errors;
|
||||
public IReadOnlyList<ValidationResult> Validate(IEnumerable<TradeCardOperationType> 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<ValidationResult> 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<NavReportException>(() => sut.SubmitAsync([SomeOperation()]));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SubmitAsync_NullOperations_Throws()
|
||||
{
|
||||
var handler = new FakeHttpMessageHandler(HttpStatusCode.OK, OkResponseXml());
|
||||
var sut = CreateSut([], handler);
|
||||
|
||||
await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => sut.SubmitAsync(null!));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tesztek az <see cref="EkaerTradeCardValidator"/>-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).
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public sealed class EkaerTradeCardValidatorTests
|
||||
{
|
||||
private static readonly EkaerTradeCardValidator Sut = new();
|
||||
|
||||
// ---- Helpers ------------------------------------------------------------
|
||||
|
||||
/// <summary>Teljesen érvényes Create művelet, egy érvényes tétellel. A tesztek ezt „rontják el".</summary>
|
||||
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<ValidationResult> 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<TradeCardOperationType>());
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public sealed class EkaerSubmitResult
|
||||
{
|
||||
private EkaerSubmitResult(bool isValid, IReadOnlyList<ValidationResult> validationErrors, ManageTradeCardsResponse? response)
|
||||
{
|
||||
IsValid = isValid;
|
||||
ValidationErrors = validationErrors;
|
||||
Response = response;
|
||||
}
|
||||
|
||||
/// <summary>Igaz, ha a validáció hibátlan volt (és így a beküldés megtörtént).</summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>A validációs hibák (üres, ha érvényes volt). Ha nem üres, a bejelentés NEM lett elküldve.</summary>
|
||||
public IReadOnlyList<ValidationResult> ValidationErrors { get; }
|
||||
|
||||
/// <summary>A NAV-válasz, ha a beküldés megtörtént; egyébként <c>null</c>.</summary>
|
||||
public ManageTradeCardsResponse? Response { get; }
|
||||
|
||||
/// <summary>Validációs hibák miatt elutasított beküldés (nem ment ki kérés a NAV-nak).</summary>
|
||||
public static EkaerSubmitResult Invalid(IReadOnlyList<ValidationResult> validationErrors) => new(false, validationErrors, null);
|
||||
|
||||
/// <summary>Sikeresen elküldött beküldés, a NAV-válasszal.</summary>
|
||||
public static EkaerSubmitResult Sent(ManageTradeCardsResponse response) => new(true, [], response);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <inheritdoc cref="IEkaerSubmitService"/>
|
||||
/// <remarks>
|
||||
/// A NAV-küldést az <see cref="EkaerManageService"/> végzi (auth + HTTP), a validációt az
|
||||
/// <see cref="IEkaerTradeCardValidator"/>. Ez az osztály csak a sorrendet (validate → send) köti egységbe.
|
||||
/// </remarks>
|
||||
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<EkaerSubmitResult> SubmitAsync(IReadOnlyList<TradeCardOperationType> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <inheritdoc cref="IEkaerTradeCardValidator"/>
|
||||
/// <remarks>
|
||||
/// Két forrásból gyűjt hibát:
|
||||
/// <list type="number">
|
||||
/// <item>a generált modellek <see cref="ValidationAttribute"/>-jai (Required / RegularExpression / Min-MaxLength),
|
||||
/// rekurzívan a tradeCard + tételek + jármű + helyszínek fölött — ezt a <see cref="Validator"/> végzi;</item>
|
||||
/// <item>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.</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class EkaerTradeCardValidator : IEkaerTradeCardValidator
|
||||
{
|
||||
public IReadOnlyList<ValidationResult> Validate(TradeCardOperationType operation)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(operation);
|
||||
var errors = new List<ValidationResult>();
|
||||
|
||||
ValidateOperation(operation, "tradeCardOperation", errors);
|
||||
return errors;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ValidationResult> Validate(IEnumerable<TradeCardOperationType> operations)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(operations);
|
||||
var list = operations.ToList();
|
||||
var errors = new List<ValidationResult>();
|
||||
|
||||
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<ValidationResult> 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<ValidationResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A generált modell <see cref="ValidationAttribute"/>-jainak ellenőrzése (Required / pattern / hossz),
|
||||
/// property-szelektíven. A generátor a nullable érték-mezőkhöz <c>*Value</c> + <c>*Specified</c> segéd-property-ket
|
||||
/// készít, és a tényleges attribútumok ezeken ülnek; a <c>*Value</c> 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 <c>*Specified</c>
|
||||
/// flag alapján dönt a kiírásról, így a default <c>*Value</c> sosem kerül ki a beküldött XML-be.
|
||||
/// </summary>
|
||||
private static void ValidateAnnotations(object instance, string path, List<ValidationResult> 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<ValidationResult>();
|
||||
|
||||
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<ValidationResult> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
errors.Add(Error(path, message));
|
||||
}
|
||||
|
||||
private static ValidationResult Error(string path, string message) => new($"{path}: {message}");
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// Á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.
|
||||
/// </summary>
|
||||
public interface IEkaerSubmitService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validálja, majd — ha hibátlan — elküldi a tradeCard műveleteket a NAV-nak.
|
||||
/// Validációs hiba esetén <see cref="EkaerSubmitResult.Invalid"/> (nem megy ki kérés);
|
||||
/// NAV-oldali hiba esetén <c>NavReportException</c> propagál.
|
||||
/// </summary>
|
||||
Task<EkaerSubmitResult> SubmitAsync(IReadOnlyList<TradeCardOperationType> operations, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using AyCode.Services.Nav.Ekaer.Models;
|
||||
|
||||
namespace AyCode.Services.Nav.Ekaer;
|
||||
|
||||
/// <summary>
|
||||
/// Az EKÁER tradeCard műveletek beküldés-előtti ellenőrzése. A NAV-séma (a generált modellek
|
||||
/// <see cref="ValidationAttribute"/>-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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Á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: <c>Nav/docs/EKAER_VALIDATION.md</c>.
|
||||
/// </remarks>
|
||||
public interface IEkaerTradeCardValidator
|
||||
{
|
||||
/// <summary>Egyetlen művelet ellenőrzése. Üres lista = érvényes.</summary>
|
||||
IReadOnlyList<ValidationResult> Validate(TradeCardOperationType operation);
|
||||
|
||||
/// <summary>Több művelet ellenőrzése; a hibák a művelet indexével prefixelve. Üres lista = mind érvényes.</summary>
|
||||
IReadOnlyList<ValidationResult> Validate(IEnumerable<TradeCardOperationType> operations);
|
||||
}
|
||||
Loading…
Reference in New Issue