FruitBankHybridApp/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs

175 lines
9.7 KiB
C#

using AyCode.Services.Nav;
using AyCode.Services.Nav.Ekaer;
using AyCode.Services.Nav.Ekaer.Models;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Services.Ekaer;
namespace FruitBank.Common.Server.Services.Ekaer;
/// <inheritdoc cref="IFruitBankEkaerService"/>
/// <remarks>
/// A teljes lánc: <c>map</c> (<see cref="IShippingToEkaerMapper"/>, FruitBank.Common) →
/// <c>validate → send</c> (<see cref="IEkaerSubmitService"/>, AyCode.Services). A saját cégadatot
/// (<see cref="EkaerSettings.Company"/>) és a NAV-fiók hitelesítő adatait a DI szolgáltatja.
/// </remarks>
public sealed class FruitBankEkaerService : IFruitBankEkaerService
{
private readonly IShippingToEkaerMapper _mapper;
private readonly IEkaerSubmitService _submitService;
private readonly IEkaerTradeCardValidator _validator;
private readonly IEkaerSettings _settings;
public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, IEkaerSettings settings)
{
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_submitService = submitService ?? throw new ArgumentNullException(nameof(submitService));
_validator = validator ?? throw new ArgumentNullException(nameof(validator));
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
public Task<EkaerSubmitResult> SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(shipping);
// map (FruitBank.Common) → submit: validate → send (AyCode.Services)
var operations = _mapper.MapShipping(shipping, _settings.Company, operation);
return _submitService.SubmitAsync(operations, cancellationToken);
}
public EkaerHistory GenerateEkaerXmlDocument(IReadOnlyCollection<ShippingDocument> documents, EkaerHistory? ekaerHistory = null)
{
ArgumentNullException.ThrowIfNull(documents);
if (documents.Count == 0) throw new ArgumentException("documents is empty", nameof(documents));
ekaerHistory ??= new EkaerHistory { IsOutgoing = false };
// A csoport azonos Partneré (a kapu így csoportosít) → az első dokumentum pénzneme a mérvadó.
var currency = documents.First().Partner?.Currency;
// Guardok a leképezés ELŐTT (nincs félrevezető XML), majd a csoport ÖSSZES dokumentuma EGY tradeCard-dá
// (összevont tömeg/érték): ToConsignment → BuildTradeCard.
return TryDepotError(ekaerHistory, documents)
?? TryConfigError(ekaerHistory, currency)
?? Finalize(ekaerHistory, _mapper.BuildTradeCard(_mapper.ToConsignment(documents, _settings.Company)), currency);
}
public EkaerHistory GenerateEkaerXmlDocument(OrderDto order, EkaerHistory? ekaerHistory = null)
{
ArgumentNullException.ThrowIfNull(order);
ekaerHistory ??= new EkaerHistory { IsOutgoing = true };
// Kimenő pénznem: jelenleg minden HUF (a deviza az OrderDto-ba kerül, amint bekötik) → ConversionRate = 1.
const string currency = "HUF";
return TryConfigError(ekaerHistory, currency) ?? Finalize(ekaerHistory, _mapper.MapOrder(order, _settings.Company), currency);
}
public EkaerObligationResult EvaluateObligation(IReadOnlyCollection<ShippingDocument> documents)
=> EkaerReportability.Evaluate(_mapper.ToConsignment(documents, _settings.Company), _settings);
public EkaerObligationResult EvaluateObligation(OrderDto order)
=> EkaerReportability.Evaluate(_mapper.ToConsignment(order, _settings.Company), _settings);
public void SetSummary(EkaerHistory history, DateTime? shippingDate, string? partner)
{
history.ShippingDate = shippingDate;
history.Partner = partner;
}
public async Task<EkaerHistory> SendEkaerAsync(EkaerHistory ekaerHistory, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(ekaerHistory);
// Csak legenerált, blokkoló hiba nélküli (warninggal is) sor küldhető — a kliens is így gátol, de itt is védünk.
if (!ekaerHistory.Status.IsSubmittable() || string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc))
{
ekaerHistory.Status = EkaerStatus.SendError;
ekaerHistory.ErrorText = "[Error] A bejelentés nem küldhető be: nincs legenerált, érvényes XML — előbb Generate.";
return ekaerHistory;
}
// A warning-eredetet a küldés ELŐTT rögzítjük (siker esetén ez dönt Sent vs SentWithMissingData között).
var hadWarning = ekaerHistory.Status == EkaerStatus.GeneratedWithWarning;
try
{
// A TÁROLT tradeCard-ot küldjük (amit a Generate előállított + validált), create-műveletként.
var tradeCard = NavXmlHelper.Deserialize<TradeCardType>(ekaerHistory.XmlDoc);
var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = tradeCard };
var result = await _submitService.SubmitAsync([operation], cancellationToken).ConfigureAwait(false);
// Elvileg nem fordul elő (a submittable sort a Generate már validálta), de defenzíven kezeljük.
if (!result.IsValid)
{
ekaerHistory.Status = EkaerStatus.ValidationError;
ekaerHistory.ErrorText = string.Join(Environment.NewLine, result.ValidationErrors.Select(e => $"[Error] {e.ErrorMessage}"));
return ekaerHistory;
}
// Siker: a NAV visszaadta az EKÁER-számot (tcn). Warningos generálásból SentWithMissingData (pótlásra vár), egyébként Sent.
ekaerHistory.EkaerNumber = result.Response?.TradeCardOperationsResults.FirstOrDefault()?.TradeCardInfo?.Tcn;
ekaerHistory.SentDate = DateTime.UtcNow;
ekaerHistory.Status = hadWarning ? EkaerStatus.SentWithMissingData : EkaerStatus.Sent;
// Tiszta beküldés → nincs üzenet; warningnál a meglévő szöveg (pl. hiányzó rendszám) MARAD (a pótlandó adat jelzése).
if (!hadWarning) ekaerHistory.ErrorText = null;
return ekaerHistory;
}
catch (NavReportException ex)
{
ekaerHistory.Status = EkaerStatus.SendError;
ekaerHistory.ErrorText = $"[Error] NAV ({ex.ReasonCode ?? ex.HttpStatusCode?.ToString() ?? "hiba"}): {ex.Message}";
return ekaerHistory;
}
catch (Exception ex)
{
ekaerHistory.Status = EkaerStatus.SendError;
ekaerHistory.ErrorText = $"[Error] Beküldési hiba: {ex.Message}";
return ekaerHistory;
}
}
/// <summary>Depot-kapu (T-D8R4): a PartnerDepot az EKÁER-hez kötelező — a felrakodási hely a depot címe. Hiányzó
/// vagy be nem töltött telephelynél a leképezés ELŐTT ValidationError (nincs félrevezető, partner-központi-címes XML).
/// <c>null</c> = rendben, mehet a generálás.</summary>
private static EkaerHistory? TryDepotError(EkaerHistory ekaerHistory, IReadOnlyCollection<ShippingDocument> documents)
{
if (documents.All(d => d.PartnerDepot != null)) return null;
ekaerHistory.Status = EkaerStatus.ValidationError;
ekaerHistory.ErrorText = "A szállítólevélhez nincs telephely (PartnerDepot) rendelve — EKÁER-hez kötelező (a felrakodási hely a telephely címe); pótlás után generáld újra.";
return ekaerHistory;
}
/// <summary>Config-kapu: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a leképezés ELŐTT
/// ValidationError (nincs félrevezető XML). <c>null</c> = rendben, mehet a generálás.</summary>
private EkaerHistory? TryConfigError(EkaerHistory ekaerHistory, string? currency)
{
if (EkaerValueCalculator.IsHuf(currency) || _settings.EurHufRate > 0) return null;
ekaerHistory.Status = EkaerStatus.ValidationError;
ekaerHistory.ErrorText = "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — a tétel-érték nem számolható.";
return ekaerHistory;
}
/// <summary>Közös befejezés (bejövő/kimenő): validál, szerializál; az XML validációs hibánál IS tárolódik
/// (a detail-nézethez), és rögzíti a ténylegesen alkalmazott árfolyamot (HUF → 1, külföldi → FX-ráta).</summary>
private EkaerHistory Finalize(EkaerHistory ekaerHistory, TradeCardType tradeCard, string? currency)
{
var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, TradeCard = tradeCard };
var messages = _validator.Validate(operation);
ekaerHistory.XmlDoc = NavXmlHelper.Serialize(tradeCard);
ekaerHistory.ConversionRate = EkaerValueCalculator.ResolveRateToHuf(currency, _settings.EurHufRate);
// Blokkoló hiba → ValidationError (nem küldhető); csak warning → GeneratedWithWarning (küldhető, de pótlandó);
// semmi → Generated. Az üzeneteket súlyossággal prefixeljük, hogy a detail-nézet megkülönböztesse őket.
ekaerHistory.Status = messages.HasErrors() ? EkaerStatus.ValidationError
: messages.HasWarnings() ? EkaerStatus.GeneratedWithWarning
: EkaerStatus.Generated;
// Error-ok elöl, warningok hátul (a detail-nézet soronként színez); a prefix [Error]/[Warning] hordozza a szintet.
ekaerHistory.ErrorText = messages.Count == 0 ? null
: string.Join(Environment.NewLine, messages
.OrderByDescending(m => m.GetSeverity())
.Select(m => $"[{m.GetSeverity()}] {m.ErrorMessage}"));
return ekaerHistory;
}
}