AyCode.Core/AyCode.Services/Nav/NavReportServiceBase.cs

120 lines
5.8 KiB
C#

using System.Net.Http;
using System.Text;
namespace AyCode.Services.Nav;
/// <summary>
/// Általános NAV-adatszolgáltatás base osztály. Egy helyen kezeli a NAV-rendszerekre közös send-flow-t:
/// auth-header feltöltés → XML szerializáció → HTTP POST → válasz deszerializáció → sikeresség-ellenőrzés.
/// A konkrét NAV API (EKÁER, Online Számla, …) ebből származik, és csak a saját namespace-ét / endpoint-ját
/// adja meg a <see cref="OperationPath"/>-ban; a request/response típusait pedig generikus paraméterként.
/// </summary>
/// <remarks>
/// <para><b>Platform-független:</b> a <see cref="HttpClient"/>-et a hívó injektálja (szerver vagy MAUI), így a
/// réteg nem köthető szerverhez. Böngésző-WASM-ből CORS miatt nem hívható közvetlenül — ott a hívás SignalR-en
/// át a szerverre delegálandó.</para>
/// <para><b>Redundancia-mentes:</b> a send-flow + auth-hash itt él egyszer; egy újabb NAV-integráció csak a saját
/// generált modelljeit és a <see cref="OperationPath"/>/<see cref="IsSuccess"/> implementációt adja.</para>
/// </remarks>
/// <typeparam name="TRequest">A NAV request típusa (az API generált root request-je, ami <see cref="INavRequest"/>-et implementál).</typeparam>
/// <typeparam name="TResponse">A NAV response típusa (az API generált root response-a, ami <see cref="INavResponse"/>-t implementál).</typeparam>
public abstract class NavReportServiceBase<TRequest, TResponse>
where TRequest : class, INavRequest
where TResponse : class, INavResponse
{
private readonly HttpClient _httpClient;
private readonly INavCredentials _credentials;
protected NavReportServiceBase(HttpClient httpClient, INavCredentials credentials)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_credentials = credentials ?? throw new ArgumentNullException(nameof(credentials));
}
/// <summary>
/// A NAV művelet relatív útvonala a <see cref="INavCredentials.BaseUrl"/>-hez képest,
/// pl. <c>"TradeCardManagementService/customer/manageTradeCards"</c>.
/// </summary>
protected abstract string OperationPath { get; }
/// <summary>
/// Elküldi a request-et a NAV-nak: feltölti az auth-headert, szerializál, POST-ol, deszerializál, és
/// ellenőrzi a sikerességet. Hibánál <see cref="NavReportException"/>-t dob.
/// </summary>
protected async Task<TResponse> SendAsync(TRequest request, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
ApplyAuthentication(request);
var requestXml = NavXmlHelper.Serialize(request);
var url = $"{_credentials.BaseUrl.TrimEnd('/')}/{OperationPath}";
// A NAV kötelezően text/xml content-type-ot ÉS accept-et vár (nem application/xml) — PDF §3.3.
using var content = new StringContent(requestXml, Encoding.UTF8, "text/xml");
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
httpRequest.Headers.Accept.ParseAdd("text/xml");
using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
var responseXml = await httpResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (!httpResponse.IsSuccessStatusCode)
{
throw new NavReportException(
$"NAV HTTP {(int)httpResponse.StatusCode} ({httpResponse.ReasonPhrase}). Body: {Truncate(responseXml, 2000)}",
httpStatusCode: (int)httpResponse.StatusCode);
}
TResponse response;
try
{
response = NavXmlHelper.Deserialize<TResponse>(responseXml);
}
catch (Exception ex)
{
throw new NavReportException($"NAV válasz deszerializáció sikertelen. Body: {Truncate(responseXml, 2000)}", innerException: ex);
}
var result = response.Result;
if (!result.IsSuccess)
{
throw new NavReportException(
result.Message ?? "NAV adatszolgáltatás sikertelen.",
reasonCode: result.ReasonCode);
}
return response;
}
/// <summary>
/// Feltölti a request base- és auth-headerét: egyedi requestId + timestamp, majd a credentials alapján a
/// felhasználó/adószám és a két SHA-512 hash (passwordHash, requestSignature). API-független, az
/// <see cref="INavRequest"/> interfészen keresztül.
/// </summary>
private void ApplyAuthentication(TRequest request)
{
var requestId = GenerateRequestId();
// A NAV az aláíráshoz a timestamp UTC-megfelelőjét várja (yyyyMMddHHmmss). UTC-t használunk, így a
// header timestamp 'Z'-vel (egyértelmű UTC) szerializálódik, és a signature ugyanazzal az értékkel
// képződik. (eKAERManagementService_2.2.pdf §2.2.3 — lásd Nav/docs/EKAER_INTERFACE.md.)
var timestamp = DateTime.UtcNow;
var header = request.RequestHeader;
header.RequestId = requestId;
header.Timestamp = timestamp;
var user = request.UserHeader;
user.User = _credentials.User;
user.TaxNumber = _credentials.TaxNumber;
user.PasswordHash = NavAuthHelper.ComputePasswordHash(_credentials.Password);
user.RequestSignature = NavAuthHelper.ComputeRequestSignature(requestId, timestamp, _credentials.SigningKey);
}
/// <summary>Egyedi requestId. EKÁER <c>BasicHeaderType.requestId</c> pattern: <c>[+a-zA-Z0-9_/=]{1,50}</c>.</summary>
private static string GenerateRequestId() => Guid.NewGuid().ToString("N");
private static string Truncate(string value, int max)
=> string.IsNullOrEmpty(value) || value.Length <= max ? value : value[..max] + "…";
}