# NCM API — Guia de integração Delphi

Guia prático para integrar a API ao seu ERP usando a unit `uNcmAPI.pas`.

## Requisitos

- **Delphi XE8 (2015) ou superior** — usa `System.Net.HttpClient` nativo.
- **HTTPS funcionando** — no Windows o `THTTPClient` usa o WinHTTP do SO,
  não precisa de DLLs do OpenSSL.
- Em macOS/Linux (FMX), funciona normalmente também.

> Se você usa Indy (`TIdHTTP`), veja a seção "Alternativa Indy" no final.

## Instalação

1. Copie `uNcmAPI.pas` para a pasta do seu projeto (ou para uma pasta comum
   de units compartilhadas).
2. Adicione na cláusula `uses`:

```pascal
uses
  uNcmAPI;
```

3. Pronto. Não tem dependência externa.

## Armazenar a API Key

Recomendado: guardar no `.ini` ou tabela de configuração do ERP, **nunca**
em código-fonte versionado.

```pascal
uses System.IniFiles, System.IOUtils;

function ObterAPIKey: string;
var
  Ini: TIniFile;
begin
  Ini := TIniFile.Create(TPath.Combine(TPath.GetDocumentsPath, 'meu_erp.ini'));
  try
    Result := Ini.ReadString('NcmAPI', 'ApiKey', '');
  finally
    Ini.Free;
  end;
end;
```

No primeiro uso, abra uma tela de configuração e salve a chave. O painel
admin entrega o `plaintext` **uma única vez** na emissão.

## Inicialização (singleton recomendado)

Como o `TNcmAPI` mantém estado de cota e conexão HTTP, vale a pena ter
um singleton no DataModule principal:

```pascal
// uDmGlobal.pas
unit uDmGlobal;

interface

uses
  System.SysUtils, uNcmAPI;

type
  TDmGlobal = class(TDataModule)
  private
    FNcmAPI: TNcmAPI;
    function GetNcmAPI: TNcmAPI;
  public
    destructor Destroy; override;
    property NcmAPI: TNcmAPI read GetNcmAPI;
  end;

var
  DmGlobal: TDmGlobal;

implementation

{$R *.dfm}

function TDmGlobal.GetNcmAPI: TNcmAPI;
begin
  if FNcmAPI = nil then
    FNcmAPI := TNcmAPI.Create(
      'https://api.seu-dominio.com.br',
      ObterAPIKey
    );
  Result := FNcmAPI;
end;

destructor TDmGlobal.Destroy;
begin
  FNcmAPI.Free;
  inherited;
end;

end.
```

Use em qualquer lugar do ERP como `DmGlobal.NcmAPI.BuscarNCM(...)`.

---

## Cenário 0 — Mostrar saldo na status bar (consulta sem cobrar)

```pascal
uses uNcmAPI, uDmGlobal;

procedure TfrmPrincipal.AtualizarSaldoNcm;
var
  S: TSaldo;
begin
  // Não cobra crédito — pode chamar à vontade
  S := DmGlobal.NcmAPI.ObterSaldo;
  if not S.Sucesso then
  begin
    StatusBar.Panels[2].Text := '❌ API NCM offline';
    Exit;
  end;

  StatusBar.Panels[2].Text := Format('NCM: %d / %d (%.1f%% disponível) — %s',
    [S.SaldoDisponivel, S.SaldoTotal, S.PercentualDisponivel, S.PlanoNome]);

  // Alerta visual quando bate < 10%
  if (S.PercentualDisponivel < 10) and (S.SaldoTotal > 0) then
    StatusBar.Panels[2].Color := clYellow;

  // Bloqueia botões se vencida ou zerada
  btnClassificar.Enabled := not S.AssinaturaVencida and (S.SaldoDisponivel > 0);
end;
```

Chamar uma vez ao abrir o sistema. Depois de cada classificação, ler os headers da resposta também atualiza sem precisar chamar `/quota` de novo.

---

## Cenário 1 — Cadastro de produto (botão "Buscar NCM")

Tela de cadastro com botão que sugere NCM/CEST/ST a partir do nome do produto.

```pascal
uses uNcmAPI, uDmGlobal;

procedure TfrmProduto.btnBuscarNcmClick(Sender: TObject);
var
  R: TNcmResultado;
begin
  if Trim(edtNome.Text) = '' then
  begin
    ShowMessage('Informe o nome do produto antes.');
    Exit;
  end;

  Screen.Cursor := crHourGlass;
  try
    try
      R := DmGlobal.NcmAPI.BuscarNCM(edtNome.Text);
    except
      on E: ENcmAPIQuota do
      begin
        ShowMessage('Sem créditos disponíveis. Contate o financeiro.');
        Exit;
      end;
      on E: ENcmAPIUnauthorized do
      begin
        ShowMessage('API Key inválida. Verifique a configuração.');
        Exit;
      end;
      on E: ENcmAPIError do
      begin
        ShowMessage('Erro na API: ' + E.Message);
        Exit;
      end;
      on E: Exception do
      begin
        ShowMessage('Erro de conexão: ' + E.Message);
        Exit;
      end;
    end;
  finally
    Screen.Cursor := crDefault;
  end;

  // Preenche os campos
  edtNCM.Text   := R.NCM;
  edtCEST.Text  := R.CEST;
  chkST.Checked := R.TemST;
  memJustif.Lines.Text := R.Justificativa;

  // Indicador visual por confiança
  case R.Confianca of
    cAlta:   edtNCM.Color := clWindow;        // branco — auto-confirma
    cMedia:  edtNCM.Color := $00B7FFFF;       // amarelo claro — revisar
    cBaixa:  edtNCM.Color := $00B7B7FF;       // vermelho claro — atenção
  end;

  // Aviso se NCM não foi validado pela tabela oficial
  if not R.NCMValidado then
    ShowMessage(
      'O NCM sugerido (' + R.NCM + ') não foi encontrado na tabela oficial.'+ #13#10 +
      'Verifique antes de salvar.'
    );

  // Atualiza barra de status com saldo
  AtualizarSaldoStatusBar;
end;

procedure TfrmProduto.AtualizarSaldoStatusBar;
begin
  if DmGlobal.NcmAPI.QuotaTotal > 0 then
    StatusBar1.Panels[1].Text := Format(
      'API NCM: %d / %d créditos',
      [DmGlobal.NcmAPI.QuotaRemaining, DmGlobal.NcmAPI.QuotaTotal]
    );
end;
```

---

## Cenário 2 — Importação em lote (thread + progresso)

Para importar 200 produtos de um Excel sem travar a UI:

```pascal
uses System.Threading, uNcmAPI, uDmGlobal;

procedure TfrmImportar.btnImportarClick(Sender: TObject);
var
  Produtos: TArray<string>;
  i: Integer;
begin
  // Monta o array a partir da grid carregada do Excel
  SetLength(Produtos, sgProdutos.RowCount - 1);
  for i := 1 to sgProdutos.RowCount - 1 do
    Produtos[i - 1] := sgProdutos.Cells[1, i];   // coluna "Nome"

  if Length(Produtos) = 0 then Exit;
  if Length(Produtos) > 500 then
  begin
    ShowMessage('Máximo 500 produtos por importação.');
    Exit;
  end;

  ProgressBar.Position := 0;
  btnImportar.Enabled := False;

  TTask.Run(
    procedure
    var
      Resultados: TArray<TNcmResultado>;
      ErroMsg: string;
    begin
      try
        Resultados := DmGlobal.NcmAPI.BuscarBatch(Produtos);
      except
        on E: Exception do
          ErroMsg := E.Message;
      end;

      TThread.Synchronize(nil,
        procedure
        var
          i: Integer;
          Cor: TColor;
        begin
          btnImportar.Enabled := True;
          if ErroMsg <> '' then begin
            ShowMessage('Erro na importação: ' + ErroMsg);
            Exit;
          end;

          for i := 0 to High(Resultados) do
          begin
            sgProdutos.Cells[2, i + 1] := Resultados[i].NCM;
            sgProdutos.Cells[3, i + 1] := Resultados[i].CEST;
            sgProdutos.Cells[4, i + 1] := IfThen(Resultados[i].TemST, 'Sim', 'Não');

            if not Resultados[i].Sucesso then
              sgProdutos.Cells[5, i + 1] := 'ERRO: ' + Resultados[i].Erro
            else
              case Resultados[i].Confianca of
                cAlta:  sgProdutos.Cells[5, i + 1] := 'OK';
                cMedia: sgProdutos.Cells[5, i + 1] := 'Revisar';
                cBaixa: sgProdutos.Cells[5, i + 1] := 'Atenção';
              end;
          end;
          ProgressBar.Position := 100;
          AtualizarSaldoStatusBar;
          ShowMessage(Format('Importação concluída. %d produtos classificados.',
                             [Length(Resultados)]));
        end);
    end);
end;
```

---

## Cenário 3 — Tela de Tributação por UF

Para emissão de NF-e: dado um NCM, mostrar todas as alíquotas e CSTs sugeridos.

```pascal
uses uNcmAPI, uDmGlobal;

procedure TfrmTributacao.AtualizarTributacao;
var
  T: TTributacao;
begin
  if (edtNCM.Text = '') or (cmbUF.Text = '') then Exit;

  Screen.Cursor := crHourGlass;
  try
    try
      T := DmGlobal.NcmAPI.ObterTributacao(
             edtNCM.Text,
             cmbUF.Text,
             rgSimples,             // ou rgPresumido / rgReal
             edtCfopInterno.Text,   // opcional, ex: '5405' (vazio = só sugestão)
             edtCfopExterno.Text,   // opcional, ex: '6404'
             atDefault              // opcional — veja nota abaixo
           );
    except
      on E: ENcmAPINotFound do
      begin
        ShowMessage('NCM não encontrado na tabela oficial.');
        Exit;
      end;
      on E: ENcmAPIError do
      begin
        ShowMessage('Erro: ' + E.Message);
        Exit;
      end;
    end;
  finally
    Screen.Cursor := crDefault;
  end;

  // Regime atual — alíquotas NOMINAIS (apenas pra exibir como referência)
  edtICMS.Text    := FormatFloat('0.00', T.AliqICMS);
  edtCstICMS.Text := T.CstICMS;
  edtPIS.Text     := FormatFloat('0.00', T.AliqPIS);
  edtCOFINS.Text  := FormatFloat('0.00', T.AliqCOFINS);
  chkST.Checked   := T.TemST;
  edtCEST.Text    := T.CEST;

  // ⚠️ IMPORTANTE — Pra EMITIR NF-e, use os campos "DestacadaNfe":
  //
  // No Simples Nacional (CSOSN 500/102/etc.) o ICMS NÃO se destaca:
  // o tributo é recolhido via DAS. Mesmo o `AliqICMS = 22.5%` informativo
  // não vai pra `<vICMS>` — vai 0. A flag `IcmsDestacarNfe` resolve isso.
  //
  // Aplicar na hora de gerar o XML:

  if T.IcmsDestacarNfe then
    NFeItem.ICMS.vICMS := BaseCalculo * T.AliqICMSDestacadaNfe / 100
  else
    NFeItem.ICMS.vICMS := 0;

  if T.PisDestacarNfe then
    NFeItem.PIS.vPIS := BaseCalculo * T.AliqPISDestacadaNfe / 100
  else
    NFeItem.PIS.vPIS := 0;

  if T.CofinsDestacarNfe then
    NFeItem.COFINS.vCOFINS := BaseCalculo * T.AliqCOFINSDestacadaNfe / 100
  else
    NFeItem.COFINS.vCOFINS := 0;

  if T.IpiDestacarNfe then
    NFeItem.IPI.vIPI := BaseCalculo * T.AliqIPI / 100
  else
    NFeItem.IPI.vIPI := 0;

  // Logar a observação fiscal pra auditoria (recomendado):
  if T.IcmsObs <> '' then
    Log('NCM ' + T.NCM + ' - ' + T.IcmsObs);

  // IBPT (transparência fiscal)
  edtCargaTotal.Text := FormatFloat('0.00', T.IbptCargaTotal) + '%';

  // Reforma Tributária
  if T.AnoReforma > 0 then
  begin
    pnlReforma.Visible := True;
    edtCBS.Text     := FormatFloat('0.0000', T.AliqCBS);
    edtIBS.Text     := FormatFloat('0.0000', T.AliqIBS);
    edtIS.Text      := IfThen(T.TemIS, FormatFloat('0.0000', T.AliqIS), '-');
    edtTotalRT.Text := FormatFloat('0.0000', T.TotalIBSCBS);
  end
  else
    pnlReforma.Visible := False;

  // Sugestão de CFOP (quando não passou o seu)
  edtCfopInterna.Text   := T.CfopInterna;
  edtCfopInteresta.Text := T.CfopInteresta;

  // 💡 Sobre o parâmetro "Atividade" (último da chamada acima):
  //
  // Define se o emitente é comércio (não destaca IPI) ou indústria (destaca).
  // A API resolve nesta ORDEM DE PRECEDÊNCIA:
  //
  //   1. Valor que você passar aqui (atComercio | atIndustria) — override pontual
  //   2. Atividade cadastrada no painel admin do tenant — recomendado
  //   3. Default por regime (Simples → comércio, Normal → indústria)
  //
  // RECOMENDADO: configure UMA VEZ no painel admin do seu tenant
  // (campo "Atividade fiscal" no detalhe do tenant) e deixe atDefault
  // aqui. Aí o seu ERP nunca mais precisa decidir essa lógica.

  // Quando você passou cfop_interno/cfop_externo, a API analisa
  // e retorna alertas se o CFOP não bate com a sujeição a ST.
  if T.CfopInternoAnalisado.Informado and (T.CfopInternoAnalisado.Alerta <> '') then
    ShowMessage('⚠ CFOP Interno: ' + T.CfopInternoAnalisado.Alerta);
  if T.CfopExternoAnalisado.Informado and (T.CfopExternoAnalisado.Alerta <> '') then
    ShowMessage('⚠ CFOP Externo: ' + T.CfopExternoAnalisado.Alerta);

  // E você pode usar o CST sugerido por CFOP em vez do CST genérico:
  // - CST genérico: T.CstICMS (baseado em regime + tem_st)
  // - CST por CFOP: T.CfopInternoAnalisado.CstIcmsSug  (mais preciso)

  // SEMPRE libere o Raw quando terminar
  if Assigned(T.Raw) then
    T.Raw.Free;
end;
```

> **Importante**: `T.Raw` é um `TJSONObject` clonado para uso avançado.
> Sempre chame `T.Raw.Free` quando terminar de usar, ou faça vazamento de memória.

---

## Tratamento de erros — referência rápida

```pascal
try
  R := Api.BuscarNCM(...);
except
  on E: ENcmAPIUnauthorized do     // 401 — chave inválida ou revogada
    LogErro('API Key inválida');

  on E: ENcmAPIQuota do             // 402 — sem créditos / vencido
    AlertarFinanceiro;

  on E: ENcmAPIForbidden do         // 403 — tenant suspenso ou key bloqueada
    LogErro('Acesso bloqueado: ' + E.Message);

  on E: ENcmAPIBadRequest do        // 400 — campo faltando, UF inválida etc
    LogErro('Requisição inválida: ' + E.Message);

  on E: ENcmAPINotFound do          // 404 — NCM não existe na tabela
    LogErro('Recurso não encontrado');

  on E: ENcmAPIError do             // 5xx ou outros HTTP
    LogErro('Erro do servidor: ' + E.Message);

  on E: Exception do                 // sem internet, timeout etc
    LogErro('Erro de rede: ' + E.Message);
end;
```

---

## Boas práticas

1. **Não consulte 1 produto por vez em loop**. Use `BuscarBatch` quando
   for processar várias linhas — é dramaticamente mais rápido e gasta
   o mesmo tanto de créditos.

2. **Salve o NCM no cadastro do produto**. Não consulte a API toda vez
   que precisar do NCM dele — só na primeira vez. Use `ObterNCM` apenas
   para revalidar periodicamente (ex: uma vez por trimestre).

3. **Para emissão de NF-e**, prefira ler o NCM/CEST do cadastro do produto
   e usar `ObterTributacao` só para pegar as alíquotas vigentes do
   estado do destinatário no momento da emissão.

4. **Cache local de tributação**: se você emite muitas NF-es seguidas
   para clientes da mesma UF/regime, salve o resultado de `ObterTributacao`
   em memória com TTL de algumas horas — economiza chamadas.

5. **Modo homologação para testes**: durante desenvolvimento e testes
   automatizados, use uma `ncm_test_xxx` separada. Não consome créditos.
   Os NCMs retornados são fictícios mas determinísticos (mesmo nome
   sempre vira o mesmo NCM mock), permitindo asserts em testes.

6. **Monitoramento de saldo**: leia `Api.QuotaRemaining` após cada chamada
   e mostre no canto inferior do ERP. Se cair abaixo de 10% do total,
   exiba um aviso ao operador.

---

## Alternativa Indy (TIdHTTP) — só se você já tem dependência

Se seu ERP já usa Indy e não quer mexer, basta substituir o método
`Request` da unit por uma versão Indy. Cole isso em uma classe
herdeira ou edite a `uNcmAPI.pas`:

```pascal
uses IdHTTP, IdSSLOpenSSL, System.JSON, System.Classes;

function RequestIndy(const Method, URL, ApiKey: string; Body: TJSONValue): string;
var
  HTTP: TIdHTTP;
  SSL:  TIdSSLIOHandlerSocketOpenSSL;
  Source: TStringStream;
begin
  HTTP := TIdHTTP.Create(nil);
  SSL  := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  Source := nil;
  try
    SSL.SSLOptions.Method := sslvTLSv1_2;
    HTTP.IOHandler := SSL;
    HTTP.Request.ContentType := 'application/json';
    HTTP.Request.Accept      := 'application/json';
    HTTP.Request.CustomHeaders.Values['X-API-Key'] := ApiKey;
    HTTP.Request.CharSet := 'utf-8';

    if SameText(Method, 'POST') then
    begin
      Source := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
      Result := HTTP.Post(URL, Source);
    end
    else
      Result := HTTP.Get(URL);
  finally
    Source.Free;
    HTTP.Free;
    SSL.Free;
  end;
end;
```

**Atenção com Indy:**
- Precisa das DLLs `libeay32.dll` e `ssleay32.dll` na pasta do executável
  (versão compatível com TLS 1.2/1.3 — recomenda-se OpenSSL 1.0.2 ou 1.1.1).
- O `TIdHTTP` por padrão NÃO atualiza propriedade pra cada header
  customizado — pra ler `X-NCM-Quota-*` use `HTTP.Response.RawHeaders.Values[...]`.
- Pra capturar status code: `HTTP.Response.ResponseCode`.

---

## Suporte

- 📧 **E-mail**: contato@uniquesistemas.com.br
- 🌐 **Documentação online**: https://api.seu-dominio.com.br/docs
- 🩺 **Status**: https://api.seu-dominio.com.br/health

Se encontrar bugs na unit ou tiver sugestões de melhoria, abra uma issue
ou mande e-mail. Pull requests bem-vindos.
