Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/Real/MoadianClient.cs
T
2026-07-13 21:49:50 +03:30

87 lines
4.3 KiB
C#

#nullable enable
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Baya.Application.Contracts.Invoices;
using Baya.Domain.Entities.Invoices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Baya.Infrastructure.CrossCutting.Seams.Real;
/// <summary>
/// Real <see cref="IMoadianClient"/> over سامانه مودیان (refinement-phase-8, 6.5). Selected by
/// <c>Seams:Moadian:Provider = moadian</c>. Submits the commission invoice (صورتحساب) to the tax-authority API and
/// maps the outcome: a returned 22-digit reference ⇒ <see cref="MoadianStatus.Registered"/>; an accepted-but-not-
/// yet-registered submission ⇒ <see cref="MoadianStatus.Submitted"/> (the reconciliation poll walks it to
/// <c>registered</c>); a rejection ⇒ <see cref="MoadianStatus.Failed"/>.
///
/// <para><b>Idempotent submit.</b> The invoice number is sent as the unique document id, so مودیان dedups a
/// re-submission — which is what lets the reconciliation job safely re-call this for a still-<c>submitted</c>
/// invoice (the seam has one verb; a re-submit doubles as the status poll). <b>Enrollment + the signing
/// certificate</b> (memory/economic id, تعهدنامه) are a deploy-time concern — a deployment supplies the memory id
/// + a current token via <c>Seams:Moadian</c>; signing the payload with the platform's private key is added inside
/// this adapter at integration without changing the seam.</para>
/// </summary>
public sealed class MoadianClient(
HttpClient httpClient,
IOptions<SeamOptions> options,
ILogger<MoadianClient> logger) : IMoadianClient
{
private const string DefaultBaseUrl = "https://tp.tax.gov.ir";
private readonly MoadianOptions _options = options.Value.Moadian;
private string BaseUrl => string.IsNullOrWhiteSpace(_options.BaseUrl) ? DefaultBaseUrl : _options.BaseUrl.TrimEnd('/');
public async ValueTask<MoadianSubmissionResult> SubmitAsync(InvoiceSubmission submission, CancellationToken cancellationToken = default)
{
var payload = new
{
memoryId = _options.MemoryId,
invoiceNumber = submission.InvoiceNumber,
bookingId = submission.BookingId,
totalAmount = submission.GrossIrr,
commissionAmount = submission.PlatformCommissionIrr,
vatAmount = submission.VatIrr,
};
using var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/req/api/self-tsp/sync/normal-enveloped")
{
Content = JsonContent.Create(payload),
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.AccessToken);
try
{
using var response = await httpClient.SendAsync(request, cancellationToken);
var raw = await response.Content.ReadAsStringAsync(cancellationToken);
if (!response.IsSuccessStatusCode)
{
logger.LogWarning("مودیان submission returned http {Http} for invoice {InvoiceNumber}",
(int)response.StatusCode, submission.InvoiceNumber);
return new MoadianSubmissionResult(MoadianStatus.Failed, null);
}
using var doc = JsonDocument.Parse(raw);
var root = doc.RootElement;
// A returned reference number registers the invoice; otherwise it was accepted and is awaiting the ref.
var reference = root.TryGetProperty("referenceNumber", out var refEl) ? refEl.GetString()
: root.TryGetProperty("uid", out var uid) ? uid.GetString()
: null;
return string.IsNullOrEmpty(reference)
? new MoadianSubmissionResult(MoadianStatus.Submitted, null)
: new MoadianSubmissionResult(MoadianStatus.Registered, reference);
}
catch (Exception ex) when (ex is HttpRequestException or JsonException or TaskCanceledException)
{
// A مودیان outage leaves the invoice submittable again on the next reconciliation tick — never failed
// permanently on a transient error.
logger.LogWarning(ex, "مودیان submission failed transiently for invoice {InvoiceNumber}", submission.InvoiceNumber);
return new MoadianSubmissionResult(MoadianStatus.Submitted, null);
}
}
}