qs.NotaFiscal.Pack 2.0.1

dotnet add package qs.NotaFiscal.Pack --version 2.0.1
                    
NuGet\Install-Package qs.NotaFiscal.Pack -Version 2.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="qs.NotaFiscal.Pack" Version="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="qs.NotaFiscal.Pack" Version="2.0.1" />
                    
Directory.Packages.props
<PackageReference Include="qs.NotaFiscal.Pack" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add qs.NotaFiscal.Pack --version 2.0.1
                    
#r "nuget: qs.NotaFiscal.Pack, 2.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package qs.NotaFiscal.Pack@2.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=qs.NotaFiscal.Pack&version=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=qs.NotaFiscal.Pack&version=2.0.1
                    
Install as a Cake Tool

qs.NotaFiscal.Pack

SDK oficial em .NET para integração de parceiros externos com a plataforma QS.NOTAS — emissão e cancelamento de NFS-e via API REST, além de recebimento padronizado de webhooks de eventos.

NuGet .NET License: MIT


📑 Índice


🎯 Visão geral

O SDK encapsula as chamadas ao endpoint público do QS.NOTAS (/api/parceiros/*) e padroniza o contrato do webhook, evitando que cada parceiro reimplemente serialização, autenticação por X-API-KEY, controle de CorrelationId e tratamento dos payloads de resposta.

Casos de uso cobertos:

UC Descrição Método
UC-02 Emitir NF IQsNotaFiscalClient.EmitirAsync
UC-04 Cancelar NF IQsNotaFiscalClient.CancelarAsync
UC-03 Processar webhook recebido QsNotaFiscalWebhookParser

Todas as operações são assíncronas, retornam HTTP 202 no sucesso da submissão e o status final chega via webhook. O SDK não lança exceção em erros de negócio (401, 409, 422) — use QsNotaFiscalResult<T>.


📦 Instalação

dotnet add package qs.NotaFiscal.Pack

Ou via PackageReference:

<PackageReference Include="qs.NotaFiscal.Pack" Version="1.0.0" />

⚙️ Configuração

Opção 1 — appsettings.json (recomendado)

{
  "QsNotaFiscal": {
    "BaseUrl": "https://api.qsnotas.com.br/",
    "ApiKey": "sk_live_AAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEEEEEE",
    "Timeout": "00:00:30",
    "UserAgent": "MinhaEmpresa/2.4 (+https://minhaempresa.com)"
  }
}
// Program.cs
using qs.NotaFiscal.Pack.DependencyInjection;

builder.Services.AddQsNotaFiscalClient(builder.Configuration);

Opção 2 — Code-first

builder.Services.AddQsNotaFiscalClient(opts =>
{
    opts.BaseUrl = "https://api.qsnotas.com.br/";
    opts.ApiKey  = Environment.GetEnvironmentVariable("QSNOTAS_API_KEY")!;
    opts.Timeout = TimeSpan.FromSeconds(30);
});

🔐 Segurança: nunca comite a ApiKey. Use variáveis de ambiente, User Secrets em dev ou um secret manager (Azure Key Vault, AWS Secrets Manager) em produção.

O extension method devolve um IHttpClientBuilderencadeie políticas de retry, circuit breaker, delegating handlers ou mocks nele (ver Resiliência).


📤 Uso — Emissão de NF (UC-02)

using qs.NotaFiscal.Pack.Abstractions;
using qs.NotaFiscal.Pack.Models.Requests;

public sealed class EmissaoService
{
    private readonly IQsNotaFiscalClient _client;

    public EmissaoService(IQsNotaFiscalClient client) => _client = client;

    public async Task EmitirAsync(CancellationToken ct)
    {
        var req = new EmitirNotaFiscalRequest
        {
            CorrelationId = $"PARCEIRO-{DateTime.UtcNow:yyyy}-{Guid.NewGuid():N}",
            Emissor       = "12345678000190",
            DataCompetencia = DateTime.Today,
            DataVencimento  = DateTime.Today.AddDays(10),
            Descricao     = "Consultoria em Tecnologia da Informação",
            CodigoListaServicos = "1.07",
            NaturezaOperacao = 1,
            Valor         = 5000.00m,
            EstadoTributacao    = "MG",
            MunicipioTributacao = "3106200", // código IBGE do município de tributação

            Tomador = new Tomador
            {
                CpfCnpj      = "12345678000199",
                RazaoSocial  = "Empresa Tomadora Ltda",
                Email        = "financeiro@tomadora.com.br",
                Logradouro   = "Av. Afonso Pena",
                Numero       = "1000",
                Bairro       = "Centro",
                Cidade       = "Belo Horizonte", // grafia exata (com acentos)
                Estado       = "MG",             // UF (2 letras)
                Cep          = "30130921",
                CodigoIbge   = "3106200"         // opcional — ver seção abaixo
            },

            Imposto = new Imposto
            {
                Valor                 = 5000.00m,
                BaseCalculoPercentual = 100,
                IssPercentual         = 5.0m,
                IssRetido             = false
            }
        };

        var result = await _client.EmitirAsync(req, ct);

        if (result.IsSuccess)
        {
            var resp = result.Value!;
            Console.WriteLine($"Aceita. NotaFiscalId={resp.NotaFiscalId}, Correlation={resp.CorrelationId}");
            // → Status final virá via webhook (ver seção abaixo).
            return;
        }

        // Erros de negócio (401, 409, 422) — sem exceção.
        Console.WriteLine($"Falha HTTP {result.HttpStatus}: {result.ErrorMessage}");
    }
}

O que esperar:

HTTP Código de erro Significado
202 Accepted Requisição aceita. NotaFiscalId e CorrelationId retornados. Status final via webhook.
400 Bad Request CorrelationId ausente.
401 Unauthorized API-KEY inválida ou ausente.
409 Conflict CorrelationId já utilizado.
422 Unprocessable Entity TOMADOR_INCOMPLETO Dados obrigatórios do tomador faltando (RazaoSocial, Logradouro, Cidade, Estado, Cep).
422 Unprocessable Entity CPFCNPJ_INVALIDO CPF/CNPJ com dígito verificador inválido.
422 Unprocessable Entity CIDADE_OBRIGATORIA Cidade/Estado ausentes e CodigoIbge também ausente.
422 Unprocessable Entity CIDADE_NAO_CADASTRADA Município não encontrado na base de cidades do QS.NOTAS — ver próxima seção.
422 Unprocessable Entity CODIGO_IBGE_INVALIDO CodigoIbge informado não corresponde a nenhum município da tabela.

🏙️ Código IBGE do município — duas formas de informar

A partir da v1.0 o SDK aceita o município do tomador de duas formas. Use a que for mais conveniente ao seu cadastro interno:

1. Via CodigoIbge (recomendado quando você já tem o código no seu cadastro):

Tomador = new Tomador
{
    // … demais campos …
    CodigoIbge = "3106200"   // 7 dígitos, fonte IBGE
}

Quando CodigoIbge é informado, o QS.NOTAS apenas valida sua existência na tabela interna de cidades e o utiliza diretamente — sem lookup por nome.

2. Via Cidade + Estado (resolução automática):

Tomador = new Tomador
{
    // … demais campos …
    Cidade = "Belo Horizonte", // grafia exata (com acentos)
    Estado = "MG"              // UF, 2 letras maiúsculas
    // CodigoIbge ausente → QS.NOTAS resolve a partir de Cidade+Estado
}

Regra de precedência: se ambos forem enviados, CodigoIbge prevalece e os campos Cidade/Estado são usados apenas para cadastro/exibição.

Implicações da resolução por nome:

  • Grafia importa. "Belo Horizonte" resolve; "belo horizonte", "Belo horizonte" ou "Bh" podem não resolver. Use sempre a grafia oficial, com acentos.
  • UF obrigatória e exata. Dois caracteres maiúsculos ("MG", não "Minas Gerais").
  • Falha se o município não existe na base. Resposta: HTTP 422 com código CIDADE_NAO_CADASTRADA. Se isso acontecer, prefira enviar via CodigoIbge ou contate o suporte — pode ser cidade recém-emancipada.

💡 Dica: o campo MunicipioTributacao do EmitirNotaFiscalRequest sempre é o código IBGE (7 dígitos) do município onde o ISS é devido — não aceita nome. É independente do município do tomador.

Como tratar no seu código:

var result = await _client.EmitirAsync(req, ct);

if (result.IsFailure && result.Errors.Any(e =>
        e.Codigo == "CIDADE_NAO_CADASTRADA" || e.Codigo == "CODIGO_IBGE_INVALIDO"))
{
    // Trate como erro de cadastro: revise os dados do tomador antes de reenviar.
    _logger.LogError("Município inválido: {Msg}", result.ErrorMessage);
    throw new InvalidOperationException(result.ErrorMessage);
}

❌ Uso — Cancelamento de NF (UC-04)

var req = new CancelarNotaFiscalRequest
{
    CorrelationId = $"PARCEIRO-CANCEL-{Guid.NewGuid():N}",
    NumeroNota    = 1234,
    Motivo        = "Serviço não prestado"
};

var result = await _client.CancelarAsync(req, ct);

if (result.IsFailure)
{
    if (result.HttpStatus == 404)
        throw new InvalidOperationException("Nota não encontrada.");
    if (result.HttpStatus == 422)
        throw new InvalidOperationException($"Prazo expirado ou dados inválidos: {result.ErrorMessage}");
}

Restrições:

  • Somente NFs com status Emitida podem ser canceladas.
  • O prazo legal de cancelamento varia por município.
  • Uma NF só pode ser cancelada pelo AccountID que a emitiu.

📥 Recebimento de Webhook (UC-03)

Configure no painel do QS.NOTAS (Configurações → Webhook) a URL do seu endpoint e a API-KEY que o QS.NOTAS deve apresentar ao chamá-lo.

Exemplo ASP.NET Core (Minimal API)

using qs.NotaFiscal.Pack.Webhook;
using qs.NotaFiscal.Pack.Models.Webhook;

var parser = app.Services.GetRequiredService<QsNotaFiscalWebhookParser>();

app.MapPut("/webhooks/qsnotas", async (HttpContext ctx, IConfiguration cfg) =>
{
    // 1. Autenticar — compare em tempo constante para evitar timing attacks.
    var expected = cfg["QsNotaFiscal:WebhookApiKey"]!;
    var received = ctx.Request.Headers["X-API-KEY"].ToString();
    if (!parser.IsApiKeyValid(received, expected))
        return Results.Unauthorized();

    // 2. Desserializar.
    WebhookEventoPayload payload;
    try
    {
        payload = await parser.ParseAsync(ctx.Request.Body, ctx.RequestAborted);
    }
    catch
    {
        return Results.BadRequest("Payload inválido.");
    }

    // 3. Processar (idempotente: o QS.NOTAS faz retries em não-200).
    switch (payload.Status)
    {
        case StatusEventoWebhook.Emitida:
            await MarcarComoEmitida(payload.CorrelationId, payload.NumeroNota, payload.Protocolo);
            break;
        case StatusEventoWebhook.Cancelada:
            await MarcarComoCancelada(payload.CorrelationId);
            break;
        case StatusEventoWebhook.Rejeitada:
        case StatusEventoWebhook.Erro:
            await RegistrarFalha(payload.CorrelationId, payload.Erros);
            break;
    }

    return Results.Ok();
});

Exemplo com Controller clássico

[ApiController]
[Route("webhooks/qsnotas")]
public sealed class QsNotasWebhookController : ControllerBase
{
    private readonly QsNotaFiscalWebhookParser _parser;
    private readonly string _expectedKey;

    public QsNotasWebhookController(QsNotaFiscalWebhookParser parser, IConfiguration cfg)
    {
        _parser = parser;
        _expectedKey = cfg["QsNotaFiscal:WebhookApiKey"]!;
    }

    [HttpPut]
    public async Task<IActionResult> Receive(CancellationToken ct)
    {
        if (!_parser.IsApiKeyValid(Request.Headers[QsNotaFiscalWebhookParser.ApiKeyHeader], _expectedKey))
            return Unauthorized();

        var payload = await _parser.ParseAsync(Request.Body, ct);
        // ... seu processamento aqui (salvar no banco, disparar evento interno, etc.)
        return Ok();
    }
}

⚠️ Idempotência é obrigatória

O QS.NOTAS re-envia (backoff exponencial, até 5 tentativas) se o seu endpoint não retornar HTTP 200. Sempre processe o webhook como idempotente: chave = CorrelationId.

if (await _db.WebhookJaProcessado(payload.CorrelationId)) return Results.Ok();

Política de retry do QS.NOTAS

Tentativa Atraso acumulado
1 imediata
2 +1 min
3 +5 min
4 +30 min
5 +2 h

Após 5 falhas, a entrega é marcada como FalhaDefinitiva no painel do QS.NOTAS.


🚨 Tratamento de erros

O SDK segue o princípio: resultado esperado → Result; imprevisto → exceção.

Situação Tipo retornado
HTTP 2xx QsNotaFiscalResult<T>.IsSuccess == true
HTTP 4xx (erro de negócio) QsNotaFiscalResult<T>.IsFailure == true com lista de Errors
HTTP 5xx após retry QsNotaFiscalResult<T>.IsFailure == true (5xx chega aqui se você esgotar retries)
Timeout / rede throw QsNotaFiscalException
Corpo JSON malformado throw QsNotaFiscalException
ArgumentException em parâmetros inválidos (null, CorrelationId vazio) throw ArgumentException
try
{
    var result = await _client.EmitirAsync(req, ct);
    if (result.IsFailure)
    {
        _logger.LogWarning("Erro {Status}: {Msg}", result.HttpStatus, result.ErrorMessage);
        return;
    }
    // sucesso…
}
catch (QsNotaFiscalException ex)
{
    // infra (rede, DNS, timeout) — reemita depois.
    _logger.LogError(ex, "Falha infra ao chamar QS.NOTAS.");
}

🔑 CorrelationId — estratégia

O CorrelationId é a chave idempotente da operação.

Regras:

  • Você decide o formato (ex.: PARCEIRO-<ano>-<id-interno>).
  • Deve ser único por emissão (reenviar o mesmo → HTTP 409).
  • O cancelamento pode ter um CorrelationId diferente da emissão.
  • O QS.NOTAS devolve o mesmo CorrelationId em todos os webhooks da NF.

Boas práticas:

  • Gere com prefixo identificável (MINHA-EMPRESA- ajuda em troubleshooting cross-sistema).
  • Nunca reuse. Se precisar reemitir após falha, gere um novo.
  • Armazene junto da sua entidade local — é como você correlaciona o webhook de volta.

🔁 Resiliência (retry / circuit breaker)

O SDK não impõe políticas de resiliência — você pluga no pipeline do HttpClient.

Com Microsoft.Extensions.Http.Resilience (recomendado .NET 8+)

dotnet add package Microsoft.Extensions.Http.Resilience
services.AddQsNotaFiscalClient(configuration)
        .AddStandardResilienceHandler(); // retry + circuit breaker + timeout

Com Polly (tradicional)

services.AddQsNotaFiscalClient(configuration)
        .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));

❗ Cuidado: retries automáticos não são seguros em operações que criam recurso (emissão). Use o CorrelationId como garantia — a API rejeita duplicatas com 409, então um retry após timeout ambíguo fica protegido.


📊 Observabilidade

O SDK emite logs via ILogger<QsNotaFiscalClient> (níveis Warning / Error). Nada de logs Information verbosos — você controla seu próprio nível de ruído.

Para capturar detalhes finos do HTTP (headers, corpo, timing), habilite o logger nativo do HttpClient:

{
  "Logging": {
    "LogLevel": {
      "System.Net.Http.HttpClient.IQsNotaFiscalClient": "Information"
    }
  }
}

Para distributed tracing (OpenTelemetry), o HttpClient já instrumenta spans automaticamente — basta registrar o HttpClient instrumentor:

services.AddOpenTelemetry()
        .WithTracing(t => t.AddHttpClientInstrumentation());

🧪 Testes

IQsNotaFiscalClient é uma interface — stub com seu mocker favorito:

// NSubstitute
var client = Substitute.For<IQsNotaFiscalClient>();
client.EmitirAsync(Arg.Any<EmitirNotaFiscalRequest>(), default)
      .Returns(QsNotaFiscalResult<EmitirNotaFiscalResponse>.Ok(
          new EmitirNotaFiscalResponse { CorrelationId = "X", NotaFiscalId = Guid.NewGuid() }, 202));

Para testes de integração com HttpClient, use Microsoft.AspNetCore.Mvc.Testing ou um DelegatingHandler customizado plugado via AddHttpMessageHandler.


🧬 Compatibilidade

Target Status Observação
netstandard2.0 Compat máxima (NETFx 4.6.1+, Mono, Xamarin).
net6.0 LTS.
net8.0 Recomendado.

Dependências principais:

  • Microsoft.Extensions.Http 8.0.0
  • Microsoft.Extensions.Options.DataAnnotations 8.0.0
  • System.Text.Json 8.0.5 (apenas para netstandard2.0)

🏗️ Princípios de design

  • Typed HttpClient via IHttpClientFactory — gestão correta do ciclo de vida do HttpClient (evita SocketException por esgotamento de portas).
  • Options pattern com validação por DataAnnotations e ValidateOnStart — falha no boot se a configuração estiver incompleta.
  • Result type para erros esperados, exceção para inesperados — clean-code PoEAA.
  • Sem dependências pesadas — nenhum Polly, AutoMapper ou Newtonsoft embutido. O parceiro escolhe o que plugar.
  • Imutabilidade onde pertinente (erros são readonly, result é sealed).
  • Cancellation token em todos os métodos async.
  • XML docs em toda a superfície pública — IntelliSense rico.
  • XML comments + Source Link — navegação até o fonte no debug.
  • Multi-target para alcançar a base instalada real do ecossistema .NET.

🛠️ Suporte

  • 📧 suporte@qsnotas.com.br
  • 🐛 Issues: github.com/<org>/qsnotafiscal/issues
  • 📚 Docs API: docs/integracoes-parceiro/integracao-parceiros.md

📄 Licença

MIT — veja o arquivo LICENSE no repositório.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.1 23 5/25/2026
1.0.1 95 4/25/2026
1.0.0 86 4/25/2026