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