120 lines
5.8 KiB
C#
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] + "…";
|
|
}
|