#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; /// /// Real over سامانه مودیان (refinement-phase-8, 6.5). Selected by /// Seams:Moadian:Provider = moadian. Submits the commission invoice (صورتحساب) to the tax-authority API and /// maps the outcome: a returned 22-digit reference ⇒ ; an accepted-but-not- /// yet-registered submission ⇒ (the reconciliation poll walks it to /// registered); a rejection ⇒ . /// /// Idempotent submit. 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-submitted /// invoice (the seam has one verb; a re-submit doubles as the status poll). Enrollment + the signing /// certificate (memory/economic id, تعهدنامه) are a deploy-time concern — a deployment supplies the memory id /// + a current token via Seams:Moadian; signing the payload with the platform's private key is added inside /// this adapter at integration without changing the seam. /// public sealed class MoadianClient( HttpClient httpClient, IOptions options, ILogger 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 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); } } }