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