using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using System.Net.Http.Headers; using System.Net.Http.Json; public interface INuvemFiscalTokenProvider { Task GetTokenAsync(CancellationToken ct = default); } internal sealed class NuvemFiscalTokenProvider( HttpClient http, IOptions opt, IMemoryCache cache, ILogger log) : INuvemFiscalTokenProvider { private readonly NuvemFiscalOptions _cfg = opt.Value; private const string CacheKey = "NuvemFiscal:AccessToken"; public async Task GetTokenAsync(CancellationToken ct = default) { if (cache.TryGetValue(CacheKey, out string token) && !string.IsNullOrWhiteSpace(token)) return token; var form = new Dictionary { ["grant_type"] = "client_credentials", ["client_id"] = _cfg.ClientId, ["client_secret"] = _cfg.ClientSecret, ["scope"] = _cfg.Scope }; using var req = new HttpRequestMessage(HttpMethod.Post, _cfg.AuthUrl) { Content = new FormUrlEncodedContent(form) }; using var res = await http.SendAsync(req, ct); res.EnsureSuccessStatusCode(); var payload = await res.Content.ReadFromJsonAsync(cancellationToken: ct) ?? throw new InvalidOperationException("Auth vazio"); var ttl = TimeSpan.FromSeconds(Math.Max(0, payload.expires_in - _cfg.TokenRenewSkewSeconds)); cache.Set(CacheKey, payload.access_token, ttl); log.LogInformation("Token NuvemFiscal obtido com escopos: {scope}", payload.scope); return payload.access_token; } private sealed class TokenDto { public string access_token { get; set; } = default!; public int expires_in { get; set; } public string scope { get; set; } = ""; public string token_type { get; set; } = "bearer"; } }