diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe168df..dbc223f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ master, 'release/*', 'hotfix/*', 'dependabot/*', 'renovate/*', 'feature/*' ] + branches: [ master, 'release/*', 'hotfix/*', 'dependabot/*', 'renovate/*', 'feature/*', 'bugfix/*', 'chore/*', 'develop' ] pull_request: branches: [ master ] diff --git a/.gitignore b/.gitignore index 5ea40c2..d936937 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,14 @@ # ============================================================================ # NFE.io SDK v3 - Git Ignore Configuration # ============================================================================ - +client-python # ---------------------------------------------------------------------------- # Dependencies # ---------------------------------------------------------------------------- node_modules/ bower_components/ jspm_packages/ +openspec/ # ---------------------------------------------------------------------------- # Build Outputs @@ -192,15 +193,3 @@ release/ test-auth.js debug-auth.mjs -4_OpenAPI Validation.txt -1_Build.txt -2_Test (Node 22.x).txt -2_Test (Node 18.x).txt -4_Test (Node 20.x).txt -5_Test (Node 22.x).txt -1_Test (Node 18.x).txt -3_Test (Node 20.x).txt -4_Build.txt -5_OpenAPI Validation.txt -3_Build.txt -2_Test (Node 20.x).txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e60f5c..a21175f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,205 @@ Todas as mudanças notáveis neste projeto serão documentadas neste arquivo. O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/), e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/). +## [3.1.0] - 2026-02-22 + +### 🎉 Expansão Massiva de Recursos - 10 Novos Recursos Implementados + +Esta release representa uma expansão significativa do SDK, transformando-o de uma solução focada em NFS-e para uma plataforma completa de gestão de documentos fiscais eletrônicos brasileiros. + +### ✨ Novos Recursos + +#### 🚚 CT-e - Conhecimento de Transporte Eletrônico (`transportationInvoices`) + +- **Consulta via Distribuição DFe**: Acesso a CT-e recebidos automaticamente +- **Habilitação/Desabilitação**: Ativar ou desativar busca automática de CT-e para empresas +- **Configuração de NSU**: Iniciar busca a partir de NSU específico ou data +- **Download de XML**: Baixar XML do CT-e e eventos associados +- **Consulta por Chave**: Buscar CT-e específico por chave de acesso (44 dígitos) +- **Eventos**: Consultar e baixar XML de eventos do CT-e + +**Exemplos:** `examples/transportation-invoices.js` + +#### 📥 NF-e de Entrada - Distribuição (`inboundProductInvoices`) + +- **Consulta de NF-e Recebidas**: Acesso a NF-e via Distribuição DFe +- **Manifestação Automática**: Configurar manifestação automática (Ciência, Confirmação, etc.) +- **Múltiplos Ambientes**: Suporte a Produção e Homologação SEFAZ +- **Download de XML Completo**: Baixar XML completo da NF-e +- **Consulta Detalhada**: Buscar por chave de acesso com informações completas +- **Gestão de Configuração**: Ativar/desativar busca automática por empresa + +**Exemplos:** `examples/inbound-product-invoices.js` + +#### 📋 Consulta NF-e por Chave (`productInvoiceQuery`) + +- **Busca Detalhada**: Consultar NF-e emitida por chave de acesso (44 dígitos) +- **Informações Completas**: Emitente, destinatário, itens, impostos, transporte, pagamento +- **Eventos Associados**: Consultar cancelamento, carta de correção, etc. +- **Validação**: Verificar situação da NF-e na SEFAZ + +**Exemplos:** `examples/product-invoice-query.js` + +#### 🧾 Consulta CFe-SAT - Cupom Fiscal Eletrônico (`consumerInvoiceQuery`) + +- **Consulta por Chave**: Buscar cupom fiscal SAT por chave de acesso +- **Informações de Venda**: Emitente, comprador, itens, pagamento +- **Impostos Detalhados**: ICMS, PIS/PASEP, COFINS, ISSQN por item +- **Validação de Status**: Verificar status do cupom fiscal + +**Exemplos:** `examples/consumer-invoice-query.js` + +#### 🏢 Consulta CNPJ - Legal Entity Lookup (`legalEntityLookup`) + +- **Informações Básicas**: Razão social, nome fantasia, regime tributário, porte, status +- **Inscrições Estaduais por Estado**: Consultar IE específica de qualquer estado +- **IE para Nota Fiscal**: Obter IE válida para emissão de NF-e/NFS-e +- **Dados Cadastrais Completos**: Endereço, telefone, atividades econômicas, sócios +- **Validação de CNPJ**: Verificar se CNPJ está ativo e regular + +**Exemplos:** `examples/cnpj-lookup.js` + +#### 👤 Consulta CPF - Natural Person Lookup (`naturalPersonLookup`) + +- **Validação de CPF**: Verificar situação cadastral na Receita Federal +- **Status Detalhado**: Regular, Pendente de Regularização, Cancelado, Suspenso, etc. +- **Integração com Cadastro**: Validar CPF antes de criar pessoa física + +**Exemplos:** `examples/cpf-lookup.js` + +#### 📊 NF-e de Produto - Emissão (`productInvoices`) + +- **Criação de NF-e**: Emitir Nota Fiscal Eletrônica de Produto +- **Cancelamento**: Cancelar NF-e com justificativa +- **Carta de Correção**: Enviar eventos de correção +- **Download de Documentos**: Baixar PDF e XML da NF-e +- **Consulta de Status**: Verificar situação da NF-e +- **Gestão de Eventos**: Consultar todos os eventos associados + +**Exemplos:** `examples/product-invoices.js` + +#### 🧮 Cálculo de Impostos (`taxCalculation`) + +- **Cálculo Automático**: ICMS, IPI, PIS, COFINS, Imposto de Importação +- **Múltiplos Itens**: Calcular impostos para vários produtos em uma requisição +- **Regimes Tributários**: Suporte a Simples Nacional, Lucro Real, Lucro Presumido +- **Origem e Destino**: Cálculo considerando estados de origem e destino +- **Detalhamento por Item**: Impostos calculados individualmente para cada item + +**Exemplos:** `examples/tax-calculation.js` + +#### 📖 Códigos Auxiliares de Tributação (`taxCodes`) + +- **Lista de CFOP**: Códigos Fiscais de Operações e Prestações +- **Lista de NCM**: Nomenclatura Comum do Mercosul +- **Origens de Mercadoria**: Códigos de origem (0-8) +- **Exportação para CSV**: Exportar listas completas + +**API:** Métodos `listCfop()`, `listNcm()`, `listOrigins()` + +#### 🗺️ Inscrições Estaduais por Estado (`stateTaxes`) + +- **Lista por Estado**: Obter todas as IE de uma empresa em um estado específico +- **Busca por CNPJ e Estado**: Consultar IE específica +- **Validação**: Verificar IE ativas e válidas + +**Exemplos:** `examples/state-taxes.js` + +### 🔧 Melhorias + +#### Configuração Unificada +- **Novo parâmetro `dataApiKey`**: Unifica `addressApiKey` e `cteApiKey` em uma única chave +- **Múltiplos Hosts de API**: Suporte a 4 hosts diferentes (api.nfe.io, api.nfse.io, address.api.nfe.io, nfe.api.nfe.io) +- **Fallback Automático**: `dataApiKey` faz fallback para `apiKey` se não especificado + +#### HTTP Client +- **Multi-API Support**: HTTP clients especializados para cada API externa +- **Lazy Loading**: Clientes HTTP criados apenas quando necessários +- **Configuração Flexível**: Base URLs configuráveis por tipo de API + +#### TypeScript +- **227+ Novos Tipos**: Tipos completos para todos os 10 novos recursos +- **Enums Abrangentes**: Estados brasileiros, status de documentos, métodos de pagamento, regimes tributários +- **Exportações Públicas**: Todos os tipos disponíveis para consumo externo + +### 📝 Documentação + +- **README.md**: Adicionadas seções completas para todos os 10 novos recursos (+411 linhas) +- **API.md**: Documentação detalhada de cada método novo (+1,212 linhas) +- **Exemplos Práticos**: 9 novos arquivos de exemplo com casos de uso reais +- **JSDoc Completo**: Documentação inline em todos os métodos públicos + +### 🧪 Testes + +- **11 Novos Arquivos de Teste**: Cobertura completa dos novos recursos (+3,882 linhas) +- **Testes Unitários**: Validação de parâmetros, tratamento de erros, mocks de API +- **Integração Multi-API**: Testes para diferentes hosts e configurações +- **Validação de Tipos**: Testes de TypeScript para garantir type-safety + +### 🐛 Correções + +- **CI/CD**: Adicionados triggers para branches `bugfix/*` e `chore/*` +- **.gitignore**: Corrigida entrada do diretório `client-python` +- **.gitignore**: Removidos arquivos de teste obsoletos +- **OpenAPI Specs**: Renomeado `cpf-api.yaml` para `consulta-cpf.yaml` para consistência +- **Generated Files**: Atualizados timestamps de regeneração dos tipos OpenAPI +- **Documentação**: Corrigidos headers de seção para NF-e de Produto e NF-e de Entrada + +### ⚠️ Mudanças de Configuração (Deprecação) + +#### Parâmetros Deprecados (Ainda Funcionam com Fallback) +- `addressApiKey` → use `dataApiKey` +- `cteApiKey` → use `dataApiKey` + +**Nota:** Os parâmetros antigos ainda funcionam, mas é recomendado migrar para `dataApiKey` para unificar a configuração. + +#### Exemplo de Migração +```typescript +// ❌ Antes (ainda funciona, mas deprecado) +const nfe = new NfeClient({ + apiKey: 'sua-chave-principal', + addressApiKey: 'chave-consultas', + cteApiKey: 'chave-consultas' +}); + +// ✅ Agora (recomendado) +const nfe = new NfeClient({ + apiKey: 'sua-chave-principal', + dataApiKey: 'chave-consultas' // Unificado +}); +``` + +### 📊 Estatísticas da Release + +- **Arquivos Modificados**: 58 arquivos +- **Linhas Adicionadas**: +14,176 +- **Linhas Removidas**: -102 +- **Crescimento de Recursos**: 5 → 15 (+200%) +- **Novos Tipos Exportados**: +227 tipos +- **Commits**: 17 commits +- **Período de Desenvolvimento**: 16/02/2026 - 22/02/2026 + +### 🚀 Recursos Totais Disponíveis + +1. ✅ **Service Invoices** - NFS-e (Notas Fiscais de Serviço) +2. ✅ **Companies** - Gestão de Empresas +3. ✅ **Legal People** - Pessoas Jurídicas (Tomadores/Prestadores) +4. ✅ **Natural People** - Pessoas Físicas +5. ✅ **Webhooks** - Notificações de Eventos +6. ✅ **Addresses** - Consulta de CEP +7. ✅ **Transportation Invoices** - CT-e (Transporte) 🆕 +8. ✅ **Inbound Product Invoices** - NF-e de Entrada 🆕 +9. ✅ **Product Invoice Query** - Consulta NF-e 🆕 +10. ✅ **Consumer Invoice Query** - Consulta CFe-SAT 🆕 +11. ✅ **Legal Entity Lookup** - Consulta CNPJ 🆕 +12. ✅ **Natural Person Lookup** - Consulta CPF 🆕 +13. ✅ **Product Invoices** - NF-e de Produto 🆕 +14. ✅ **Tax Calculation** - Cálculo de Impostos 🆕 +15. ✅ **Tax Codes** - Códigos Auxiliares 🆕 +16. ✅ **State Taxes** - Inscrições Estaduais 🆕 + +--- + ## [3.0.2] - 2026-01-19 ### 🐛 Correções diff --git a/README.md b/README.md index 13922b6..677781d 100644 --- a/README.md +++ b/README.md @@ -360,18 +360,417 @@ const filtrado = await nfe.addresses.search({ }); ``` -> **Nota:** A API de Endereços usa um host separado (`address.api.nfe.io`). Você pode configurar uma chave API específica com `addressApiKey`, ou o SDK usará `apiKey` como fallback. +> **Nota:** A API de Endereços usa um host separado (`address.api.nfe.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +#### 🚚 Notas de Transporte - CT-e (`nfe.transportationInvoices`) + +Consultar CT-e (Conhecimento de Transporte Eletrônico) via Distribuição DFe: + +```typescript +// Ativar busca automática de CT-e para uma empresa +const settings = await nfe.transportationInvoices.enable('empresa-id'); +console.log('Status:', settings.status); +console.log('Iniciando do NSU:', settings.startFromNsu); + +// Ativar a partir de um NSU específico +const settings = await nfe.transportationInvoices.enable('empresa-id', { + startFromNsu: 12345 +}); + +// Ativar a partir de uma data específica +const settings = await nfe.transportationInvoices.enable('empresa-id', { + startFromDate: '2024-01-01T00:00:00Z' +}); + +// Verificar configurações atuais +const config = await nfe.transportationInvoices.getSettings('empresa-id'); +console.log('Busca ativa:', config.status); + +// Desativar busca automática +await nfe.transportationInvoices.disable('empresa-id'); + +// Consultar CT-e por chave de acesso (44 dígitos) +const cte = await nfe.transportationInvoices.retrieve( + 'empresa-id', + '35240112345678000190570010000001231234567890' +); +console.log('Remetente:', cte.nameSender); +console.log('Valor:', cte.totalInvoiceAmount); +console.log('Emissão:', cte.issuedOn); + +// Baixar XML do CT-e +const xml = await nfe.transportationInvoices.downloadXml( + 'empresa-id', + '35240112345678000190570010000001231234567890' +); +fs.writeFileSync('cte.xml', xml); + +// Consultar evento do CT-e +const evento = await nfe.transportationInvoices.getEvent( + 'empresa-id', + '35240112345678000190570010000001231234567890', + 'chave-evento' +); + +// Baixar XML do evento +const eventoXml = await nfe.transportationInvoices.downloadEventXml( + 'empresa-id', + '35240112345678000190570010000001231234567890', + 'chave-evento' +); +``` + +> **Nota:** A API de CT-e usa um host separado (`api.nfse.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +**Pré-requisitos:** +- Empresa deve estar cadastrada com certificado digital A1 válido +- Webhook deve estar configurado para receber notificações de CT-e + +#### 📥 NF-e de Entrada - Distribuição (`nfe.inboundProductInvoices`) + +Consultar NF-e (Nota Fiscal Eletrônica de Produto) recebidas via Distribuição NF-e: + +```typescript +// Ativar busca automática de NF-e para uma empresa +const settings = await nfe.inboundProductInvoices.enableAutoFetch('empresa-id', { + environmentSEFAZ: 'Production', + webhookVersion: '2', +}); +console.log('Status:', settings.status); + +// Ativar a partir de um NSU específico +const settings = await nfe.inboundProductInvoices.enableAutoFetch('empresa-id', { + startFromNsu: '999999', + environmentSEFAZ: 'Production', +}); + +// Verificar configurações atuais +const config = await nfe.inboundProductInvoices.getSettings('empresa-id'); +console.log('Busca ativa:', config.status); + +// Desativar busca automática +await nfe.inboundProductInvoices.disableAutoFetch('empresa-id'); + +// Consultar NF-e por chave de acesso - formato webhook v2 (recomendado) +const nfe_doc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); +console.log('Emissor:', nfe_doc.issuer?.name); +console.log('Valor:', nfe_doc.totalInvoiceAmount); + +// Baixar XML da NF-e +const xml = await nfe.inboundProductInvoices.getXml( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('nfe.xml', xml); + +// Baixar PDF (DANFE) +const pdf = await nfe.inboundProductInvoices.getPdf( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); + +// Enviar manifestação (Ciência da Operação por padrão) +await nfe.inboundProductInvoices.manifest( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); + +// Manifestar com evento específico +await nfe.inboundProductInvoices.manifest( + 'empresa-id', + '35240112345678000190550010000001231234567890', + 210220 // Confirmação da Operação +); + +// Consultar evento da NF-e +const evento = await nfe.inboundProductInvoices.getEventDetails( + 'empresa-id', + '35240112345678000190550010000001231234567890', + 'chave-evento' +); + +// Baixar XML do evento +const eventoXml = await nfe.inboundProductInvoices.getEventXml( + 'empresa-id', + '35240112345678000190550010000001231234567890', + 'chave-evento' +); + +// Reprocessar webhook +await nfe.inboundProductInvoices.reprocessWebhook('empresa-id', '35240...'); +``` + +> **Nota:** A API de NF-e Distribuição usa um host separado (`api.nfse.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +**Pré-requisitos:** +- Empresa deve estar cadastrada com certificado digital A1 válido +- Webhook deve estar configurado para receber notificações de NF-e + +**Tipos de Manifestação:** + +| Código | Evento | +|--------|--------| +| `210210` | Ciência da Operação (padrão) | +| `210220` | Confirmação da Operação | +| `210240` | Operação não Realizada | + +#### 📦 NF-e de Produto - Emissão (`nfe.productInvoices`) + +Ciclo completo de gestão de NF-e (Nota Fiscal Eletrônica de Produto) — emissão, listagem, consulta, cancelamento, carta de correção (CC-e), inutilização e download de arquivos (PDF/XML): + +```typescript +// Emitir NF-e (assíncrono — retorna 202) +const result = await nfe.productInvoices.create('empresa-id', { + operationNature: 'Venda de mercadoria', + operationType: 'Outgoing', + buyer: { name: 'Empresa LTDA', federalTaxNumber: 12345678000190 }, + items: [{ code: 'PROD-001', description: 'Produto X', quantity: 1, unitAmount: 100 }], + payment: [{ paymentDetail: [{ method: 'Cash', amount: 100 }] }], +}); + +// Listar NF-e (environment é obrigatório) +const invoices = await nfe.productInvoices.list('empresa-id', { + environment: 'Production', + limit: 10, +}); + +// Consultar NF-e por ID +const invoice = await nfe.productInvoices.retrieve('empresa-id', 'invoice-id'); + +// Cancelar NF-e (assíncrono) +await nfe.productInvoices.cancel('empresa-id', 'invoice-id', 'Motivo do cancelamento'); + +// Download de PDF e XML +const pdf = await nfe.productInvoices.downloadPdf('empresa-id', 'invoice-id'); +const xml = await nfe.productInvoices.downloadXml('empresa-id', 'invoice-id'); + +// Carta de correção (CC-e) — razão de 15 a 1.000 caracteres +await nfe.productInvoices.sendCorrectionLetter('empresa-id', 'invoice-id', + 'Correcao do endereco do destinatario conforme novo cadastro'); + +// Inutilizar faixa de numeração +await nfe.productInvoices.disableRange('empresa-id', { + environment: 'Production', + serie: 1, + state: 'SP', + beginNumber: 100, + lastNumber: 110, +}); +``` + +> **Nota:** Operações de emissão, cancelamento, CC-e e inutilização são assíncronas — retornam 202/204. Conclusão é notificada via webhooks. + +#### 🏛️ Inscrições Estaduais (`nfe.stateTaxes`) + +CRUD de inscrições estaduais (IE) — configuração necessária para emissão de NF-e de produto: + +```typescript +// Listar inscrições estaduais +const taxes = await nfe.stateTaxes.list('empresa-id'); + +// Criar inscrição estadual +const tax = await nfe.stateTaxes.create('empresa-id', { + taxNumber: '123456789', + serie: 1, + number: 1, + code: 'sP', + environmentType: 'production', + type: 'nFe', +}); + +// Consultar, atualizar e excluir +const retrieved = await nfe.stateTaxes.retrieve('empresa-id', 'state-tax-id'); +await nfe.stateTaxes.update('empresa-id', 'state-tax-id', { serie: 2 }); +await nfe.stateTaxes.delete('empresa-id', 'state-tax-id'); +``` + +> **Nota:** Usa o host `api.nfse.io`. Configure `dataApiKey` para chave separada, ou o SDK usará `apiKey` como fallback. + +#### 🔍 Consulta de NF-e por Chave de Acesso (`nfe.productInvoiceQuery`) + +Consultar NF-e (Nota Fiscal Eletrônica de Produto) diretamente na SEFAZ por chave de acesso. Recurso somente leitura sem necessidade de escopo de empresa: + +```typescript +// Consultar dados completos da NF-e +const invoice = await nfe.productInvoiceQuery.retrieve( + '35240112345678000190550010000001231234567890' +); +console.log('Status:', invoice.currentStatus); +console.log('Emissor:', invoice.issuer?.name); +console.log('Valor:', invoice.totals?.icms?.invoiceAmount); + +// Baixar DANFE (PDF) +const pdf = await nfe.productInvoiceQuery.downloadPdf( + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('danfe.pdf', pdf); + +// Baixar XML da NF-e +const xml = await nfe.productInvoiceQuery.downloadXml( + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('nfe.xml', xml); + +// Listar eventos fiscais (cancelamentos, correções, manifestações) +const result = await nfe.productInvoiceQuery.listEvents( + '35240112345678000190550010000001231234567890' +); +for (const event of result.events ?? []) { + console.log(event.description, event.authorizedOn); +} +``` + +> **Nota:** A API de Consulta NF-e usa um host separado (`nfe.api.nfe.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +#### 🧾 Consulta de Cupom Fiscal Eletrônico - CFe-SAT (`nfe.consumerInvoiceQuery`) + +Consultar CFe-SAT (Cupom Fiscal Eletrônico) por chave de acesso. Recurso somente leitura sem necessidade de escopo de empresa: + +```typescript +// Consultar dados completos do cupom fiscal +const coupon = await nfe.consumerInvoiceQuery.retrieve( + '35240112345678000190590000000012341234567890' +); +console.log('Status:', coupon.currentStatus); // 'Authorized' +console.log('Emissor:', coupon.issuer?.name); +console.log('Valor:', coupon.totals?.couponAmount); + +// Baixar XML do CFe +const xml = await nfe.consumerInvoiceQuery.downloadXml( + '35240112345678000190590000000012341234567890' +); +fs.writeFileSync('cfe.xml', xml); +``` + +> **Nota:** A API de Consulta CFe-SAT usa o mesmo host (`nfe.api.nfe.io`) e chave de API que a consulta de NF-e. + +#### 🏢 Consulta CNPJ / Pessoa Jurídica (`nfe.legalEntityLookup`) + +Consultar dados cadastrais de empresas brasileiras (CNPJ) na Receita Federal e nas SEFAZs estaduais: + +```typescript +// Consulta básica por CNPJ (aceita com ou sem pontuação) +const result = await nfe.legalEntityLookup.getBasicInfo('12.345.678/0001-90'); +console.log('Razão Social:', result.legalEntity?.name); +console.log('Nome Fantasia:', result.legalEntity?.tradeName); +console.log('Status:', result.legalEntity?.status); // 'Active' +console.log('Porte:', result.legalEntity?.size); // 'ME', 'EPP', etc. +console.log('Cidade:', result.legalEntity?.address?.city?.name); + +// Consulta com opções +const result = await nfe.legalEntityLookup.getBasicInfo('12345678000190', { + updateAddress: false, // Não atualizar endereço via Correios + updateCityCode: true, // Atualizar código IBGE da cidade +}); + +// Consultar Inscrição Estadual (IE) por estado +const ieSP = await nfe.legalEntityLookup.getStateTaxInfo('SP', '12345678000190'); +for (const tax of ieSP.legalEntity?.stateTaxes ?? []) { + console.log(`IE: ${tax.taxNumber} - Status: ${tax.status}`); + console.log(` NFe: ${tax.nfe?.status}, CTe: ${tax.cte?.status}`); +} + +// Avaliar IE para emissão de nota fiscal +const invoice = await nfe.legalEntityLookup.getStateTaxForInvoice('MG', '12345678000190'); +for (const tax of invoice.legalEntity?.stateTaxes ?? []) { + if (tax.status === 'Abled') { + console.log(`Pode emitir com IE: ${tax.taxNumber}`); + } +} + +// Obter melhor IE sugerida para emissão +const sugestao = await nfe.legalEntityLookup.getSuggestedStateTaxForInvoice('SP', '12345678000190'); +const melhorIE = sugestao.legalEntity?.stateTaxes?.[0]; +console.log('IE recomendada:', melhorIE?.taxNumber); +``` + +> **Nota:** A API de Consulta CNPJ usa um host separado (`legalentity.api.nfe.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +#### 👤 Consulta CPF / Pessoa Física (`nfe.naturalPersonLookup`) + +Consultar a situação cadastral de CPF (pessoa física) na Receita Federal: + +```typescript +// Consulta com CPF e data de nascimento +const result = await nfe.naturalPersonLookup.getStatus('123.456.789-01', '1990-01-15'); +console.log('Nome:', result.name); // 'JOÃO DA SILVA' +console.log('Status:', result.status); // 'Regular' + +// Também aceita Date object +const result = await nfe.naturalPersonLookup.getStatus('12345678901', new Date(1990, 0, 15)); +console.log('Situação Cadastral:', result.status); +``` + +> **Nota:** A API de Consulta CPF usa um host separado (`naturalperson.api.nfe.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +#### 🧮 Cálculo de Impostos (`nfe.taxCalculation`) + +Calcular todos os tributos aplicáveis (ICMS, ICMS-ST, PIS, COFINS, IPI, II) para operações com produtos usando o Motor de Cálculo de Tributos: + +```typescript +// Calcular impostos de uma operação de venda +const resultado = await nfe.taxCalculation.calculate('tenant-id', { + operationType: 'Outgoing', + issuer: { state: 'SP', taxRegime: 'RealProfit' }, + recipient: { state: 'RJ' }, + items: [{ + id: 'item-1', + operationCode: 121, + origin: 'National', + ncm: '61091000', + quantity: 10, + unitAmount: 100.00 + }] +}); + +for (const item of resultado.items ?? []) { + console.log(`Item ${item.id}: CFOP ${item.cfop}`); + console.log(` ICMS: CST=${item.icms?.cst}, valor=${item.icms?.vICMS}`); + console.log(` PIS: CST=${item.pis?.cst}, valor=${item.pis?.vPIS}`); + console.log(` COFINS: CST=${item.cofins?.cst}, valor=${item.cofins?.vCOFINS}`); +} +``` + +> **Nota:** A API de Cálculo de Impostos usa o host `api.nfse.io`. Configure `dataApiKey` para uma chave específica, ou o SDK usará `apiKey` como fallback. + +#### 📋 Códigos Auxiliares de Impostos (`nfe.taxCodes`) + +Consultar tabelas de referência necessárias para o cálculo de impostos: + +```typescript +// Listar códigos de operação (natureza de operação) +const codigos = await nfe.taxCodes.listOperationCodes({ pageIndex: 1, pageCount: 20 }); +for (const cod of codigos.items ?? []) { + console.log(`${cod.code} - ${cod.description}`); +} + +// Listar finalidades de aquisição +const finalidades = await nfe.taxCodes.listAcquisitionPurposes(); + +// Listar perfis fiscais do emissor +const perfisEmissor = await nfe.taxCodes.listIssuerTaxProfiles(); + +// Listar perfis fiscais do destinatário +const perfisDestinatario = await nfe.taxCodes.listRecipientTaxProfiles(); +``` + +> **Nota:** Todas as listagens suportam paginação via `pageIndex` (1-based) e `pageCount` (padrão: 50). + +--- ### Opções de Configuração ```typescript const nfe = new NfeClient({ - // Chave API principal do NFE.io (opcional se usar apenas Addresses com addressApiKey) + // Chave API principal do NFE.io (operações com documentos fiscais) apiKey: 'sua-chave-api', - // Opcional: Chave API específica para consulta de endereços + // Opcional: Chave API para serviços de consulta (Endereços, CT-e, CNPJ, CPF) // Se não fornecida, usa apiKey como fallback - addressApiKey: 'sua-chave-address-api', + dataApiKey: 'sua-chave-data-api', // Opcional: Ambiente (padrão: 'production') environment: 'production', // ou 'sandbox' @@ -399,12 +798,12 @@ O SDK suporta as seguintes variáveis de ambiente: | Variável | Descrição | |----------|-----------| | `NFE_API_KEY` | Chave API principal (fallback para `apiKey`) | -| `NFE_ADDRESS_API_KEY` | Chave API para endereços (fallback para `addressApiKey`) | +| `NFE_DATA_API_KEY` | Chave API para serviços de consulta (fallback para `dataApiKey`) | ```bash # Configurar via ambiente export NFE_API_KEY="sua-chave-api" -export NFE_ADDRESS_API_KEY="sua-chave-address" +export NFE_DATA_API_KEY="sua-chave-data" # Usar SDK sem passar chaves no código const nfe = new NfeClient({}); diff --git a/docs/API.md b/docs/API.md index 4adbbc3..c9f36d2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,6 +15,16 @@ Complete API reference for the NFE.io Node.js SDK v3. - [Legal People](#legal-people) - [Natural People](#natural-people) - [Webhooks](#webhooks) + - [Transportation Invoices (CT-e)](#transportation-invoices-ct-e) + - [Inbound Product Invoices (NF-e Distribuição)](#inbound-product-invoices-nf-e-distribuição) + - [Product Invoice Query (Consulta NF-e)](#product-invoice-query-consulta-nf-e) + - [Consumer Invoice Query (Consulta CFe-SAT)](#consumer-invoice-query-consulta-cfe-sat) + - [Legal Entity Lookup (Consulta CNPJ)](#legal-entity-lookup-consulta-cnpj) + - [Natural Person Lookup (Consulta CPF)](#natural-person-lookup-consulta-cpf) + - [Tax Calculation (Cálculo de Impostos)](#tax-calculation-cálculo-de-impostos) + - [Tax Codes (Códigos Auxiliares)](#tax-codes-códigos-auxiliares) + - [Product Invoices (NF-e Emissão)](#product-invoices-nf-e-emissão) + - [State Taxes (Inscrições Estaduais)](#state-taxes-inscrições-estaduais) - [Types](#types) - [Error Handling](#error-handling) - [Advanced Usage](#advanced-usage) @@ -1504,6 +1514,1200 @@ const events = await nfe.webhooks.getAvailableEvents(); // ['invoice.issued', 'invoice.cancelled', ...] ``` +--- + +### Transportation Invoices (CT-e) + +**Resource:** `nfe.transportationInvoices` + +Manage CT-e (Conhecimento de Transporte Eletrônico) documents via SEFAZ Distribuição DFe. + +> **Note:** This resource uses a separate API host (`api.nfse.io`). You can configure a specific API key with `dataApiKey`, or the SDK will use `apiKey` as fallback. + +**Prerequisites:** +- Company must be registered with a valid A1 digital certificate +- Webhook must be configured to receive CT-e notifications + +#### `enable(companyId: string, options?: EnableTransportationInvoiceOptions): Promise` + +Enable automatic CT-e search for a company. + +```typescript +// Enable with default settings +const settings = await nfe.transportationInvoices.enable('company-id'); + +// Enable starting from a specific NSU +const settings = await nfe.transportationInvoices.enable('company-id', { + startFromNsu: 12345 +}); + +// Enable starting from a specific date +const settings = await nfe.transportationInvoices.enable('company-id', { + startFromDate: '2024-01-01T00:00:00Z' +}); +``` + +**Options:** + +| Property | Type | Description | +|----------|------|-------------| +| `startFromNsu` | `number` | Start searching from this NSU number | +| `startFromDate` | `string` | Start searching from this date (ISO 8601) | + +#### `disable(companyId: string): Promise` + +Disable automatic CT-e search for a company. + +```typescript +const settings = await nfe.transportationInvoices.disable('company-id'); +console.log('Status:', settings.status); // 'Disabled' +``` + +#### `getSettings(companyId: string): Promise` + +Get current automatic CT-e search settings. + +```typescript +const settings = await nfe.transportationInvoices.getSettings('company-id'); +console.log('Status:', settings.status); +console.log('Start NSU:', settings.startFromNsu); +console.log('Created:', settings.createdOn); +``` + +**Response:** + +| Property | Type | Description | +|----------|------|-------------| +| `status` | `string` | Current status ('Active', 'Disabled', etc.) | +| `startFromNsu` | `number` | Starting NSU number | +| `startFromDate` | `string` | Starting date (if configured) | +| `createdOn` | `string` | Creation timestamp | +| `modifiedOn` | `string` | Last modification timestamp | + +#### `retrieve(companyId: string, accessKey: string): Promise` + +Retrieve CT-e metadata by its 44-digit access key. + +```typescript +const cte = await nfe.transportationInvoices.retrieve( + 'company-id', + '35240112345678000190570010000001231234567890' +); +console.log('Sender:', cte.nameSender); +console.log('CNPJ:', cte.federalTaxNumberSender); +console.log('Amount:', cte.totalInvoiceAmount); +console.log('Issued:', cte.issuedOn); +``` + +**Response:** + +| Property | Type | Description | +|----------|------|-------------| +| `accessKey` | `string` | 44-digit access key | +| `type` | `string` | Document type | +| `status` | `string` | Document status | +| `nameSender` | `string` | Sender company name | +| `federalTaxNumberSender` | `string` | Sender CNPJ | +| `totalInvoiceAmount` | `number` | Total invoice amount | +| `issuedOn` | `string` | Issue date | +| `receivedOn` | `string` | Receipt date | + +#### `downloadXml(companyId: string, accessKey: string): Promise` + +Download CT-e XML content. + +```typescript +const xml = await nfe.transportationInvoices.downloadXml( + 'company-id', + '35240112345678000190570010000001231234567890' +); +fs.writeFileSync('cte.xml', xml); +``` + +#### `getEvent(companyId: string, accessKey: string, eventKey: string): Promise` + +Retrieve CT-e event metadata. + +```typescript +const event = await nfe.transportationInvoices.getEvent( + 'company-id', + '35240112345678000190570010000001231234567890', + 'event-key-123' +); +console.log('Event type:', event.type); +console.log('Event status:', event.status); +``` + +#### `downloadEventXml(companyId: string, accessKey: string, eventKey: string): Promise` + +Download CT-e event XML content. + +```typescript +const eventXml = await nfe.transportationInvoices.downloadEventXml( + 'company-id', + '35240112345678000190570010000001231234567890', + 'event-key-123' +); +fs.writeFileSync('cte-event.xml', eventXml); +``` + +--- + +### Inbound Product Invoices (NF-e Distribuição) + +**Resource:** `nfe.inboundProductInvoices` + +Query NF-e (Nota Fiscal Eletrônica de Produto) documents received via SEFAZ Distribuição NF-e. + +> **Note:** This resource uses a separate API host (`api.nfse.io`). You can configure a specific API key with `dataApiKey`, or the SDK will use `apiKey` as fallback. + +**Prerequisites:** +- Company must be registered with a valid A1 digital certificate +- Webhook must be configured to receive NF-e notifications + +#### `enableAutoFetch(companyId: string, options: EnableInboundOptions): Promise` + +Enable automatic NF-e inbound fetching for a company. + +```typescript +// Enable with production environment and webhook v2 +const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + environmentSEFAZ: 'Production', + webhookVersion: '2', +}); + +// Enable starting from a specific NSU +const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + startFromNsu: '999999', + environmentSEFAZ: 'Production', +}); + +// Enable with automatic manifesting +const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + environmentSEFAZ: 'Production', + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, +}); +``` + +**Options:** + +| Property | Type | Description | +|----------|------|-------------| +| `startFromNsu` | `string` | Start searching from this NSU number | +| `startFromDate` | `string` | Start searching from this date (ISO 8601) | +| `environmentSEFAZ` | `string \| null` | SEFAZ environment ('Production', etc.) | +| `automaticManifesting` | `AutomaticManifesting` | Auto-manifest configuration | +| `webhookVersion` | `string` | Webhook version ('1' or '2') | + +#### `disableAutoFetch(companyId: string): Promise` + +Disable automatic NF-e inbound fetching for a company. + +```typescript +const settings = await nfe.inboundProductInvoices.disableAutoFetch('company-id'); +console.log('Status:', settings.status); // 'Inactive' +``` + +#### `getSettings(companyId: string): Promise` + +Get current automatic NF-e inbound settings. + +```typescript +const settings = await nfe.inboundProductInvoices.getSettings('company-id'); +console.log('Status:', settings.status); +console.log('Environment:', settings.environmentSEFAZ); +console.log('Webhook version:', settings.webhookVersion); +console.log('Start NSU:', settings.startFromNsu); +``` + +**Response (`InboundSettings`):** + +| Property | Type | Description | +|----------|------|-------------| +| `status` | `string` | Current status ('Active', 'Inactive', etc.) | +| `startFromNsu` | `string` | Starting NSU number | +| `startFromDate` | `string` | Starting date (if configured) | +| `environmentSEFAZ` | `string \| null` | SEFAZ environment | +| `automaticManifesting` | `AutomaticManifesting` | Auto-manifest configuration | +| `webhookVersion` | `string` | Webhook version | +| `companyId` | `string` | Company ID | +| `createdOn` | `string` | Creation timestamp | +| `modifiedOn` | `string` | Last modification timestamp | + +#### `getDetails(companyId: string, accessKey: string): Promise` + +Retrieve NF-e metadata by 44-digit access key (webhook v1 format). + +```typescript +const nfeDoc = await nfe.inboundProductInvoices.getDetails( + 'company-id', + '35240112345678000190550010000001231234567890' +); +console.log('Issuer:', nfeDoc.issuer?.name); +console.log('Amount:', nfeDoc.totalInvoiceAmount); +console.log('Issued:', nfeDoc.issuedOn); +``` + +#### `getProductInvoiceDetails(companyId: string, accessKey: string): Promise` + +Retrieve NF-e metadata by 44-digit access key (webhook v2 format, recommended). + +```typescript +const nfeDoc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + 'company-id', + '35240112345678000190550010000001231234567890' +); +console.log('Issuer:', nfeDoc.issuer?.name); +console.log('Amount:', nfeDoc.totalInvoiceAmount); +console.log('Product invoices:', nfeDoc.productInvoices?.length); +``` + +**Response (`InboundInvoiceMetadata` / `InboundProductInvoiceMetadata`):** + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `string` | Document ID | +| `accessKey` | `string` | 44-digit access key | +| `nsu` | `string` | NSU number | +| `nfeNumber` | `string` | NF-e number | +| `issuer` | `InboundIssuer` | Issuer information | +| `buyer` | `InboundBuyer` | Buyer information | +| `totalInvoiceAmount` | `string` | Total amount | +| `issuedOn` | `string` | Issue date | +| `description` | `string` | Description | +| `links` | `InboundLinks` | XML/PDF download links | +| `productInvoices` | `InboundProductInvoice[]` | Product invoices (v2 only) | + +#### `getEventDetails(companyId: string, accessKey: string, eventKey: string): Promise` + +Retrieve NF-e event metadata (webhook v1 format). + +```typescript +const event = await nfe.inboundProductInvoices.getEventDetails( + 'company-id', + '35240112345678000190550010000001231234567890', + 'event-key-123' +); +``` + +#### `getProductInvoiceEventDetails(companyId: string, accessKey: string, eventKey: string): Promise` + +Retrieve NF-e event metadata (webhook v2 format). + +```typescript +const event = await nfe.inboundProductInvoices.getProductInvoiceEventDetails( + 'company-id', + '35240112345678000190550010000001231234567890', + 'event-key-123' +); +``` + +#### `getXml(companyId: string, accessKey: string): Promise` + +Download NF-e XML content. + +```typescript +const xml = await nfe.inboundProductInvoices.getXml( + 'company-id', + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('nfe.xml', xml); +``` + +#### `getEventXml(companyId: string, accessKey: string, eventKey: string): Promise` + +Download NF-e event XML content. + +```typescript +const eventXml = await nfe.inboundProductInvoices.getEventXml( + 'company-id', + '35240112345678000190550010000001231234567890', + 'event-key-123' +); +fs.writeFileSync('nfe-event.xml', eventXml); +``` + +#### `getPdf(companyId: string, accessKey: string): Promise` + +Download NF-e PDF (DANFE). + +```typescript +const pdf = await nfe.inboundProductInvoices.getPdf( + 'company-id', + '35240112345678000190550010000001231234567890' +); +``` + +#### `getJson(companyId: string, accessKey: string): Promise` + +Get NF-e data in JSON format. + +```typescript +const json = await nfe.inboundProductInvoices.getJson( + 'company-id', + '35240112345678000190550010000001231234567890' +); +``` + +#### `manifest(companyId: string, accessKey: string, tpEvent?: ManifestEventType): Promise` + +Send a manifest event for an NF-e. Defaults to `210210` (Ciência da Operação). + +```typescript +// Ciência da Operação (default) +await nfe.inboundProductInvoices.manifest( + 'company-id', + '35240112345678000190550010000001231234567890' +); + +// Confirmação da Operação +await nfe.inboundProductInvoices.manifest( + 'company-id', + '35240112345678000190550010000001231234567890', + 210220 +); + +// Operação não Realizada +await nfe.inboundProductInvoices.manifest( + 'company-id', + '35240112345678000190550010000001231234567890', + 210240 +); +``` + +**Manifest Event Types:** + +| Code | Event | +|------|-------| +| `210210` | Ciência da Operação (awareness, default) | +| `210220` | Confirmação da Operação (confirmation) | +| `210240` | Operação não Realizada (operation not performed) | + +#### `reprocessWebhook(companyId: string, accessKeyOrNsu: string): Promise` + +Reprocess a webhook notification by access key or NSU. + +```typescript +// By access key +await nfe.inboundProductInvoices.reprocessWebhook( + 'company-id', + '35240112345678000190550010000001231234567890' +); + +// By NSU +await nfe.inboundProductInvoices.reprocessWebhook( + 'company-id', + '12345' +); +``` + +--- + +### Product Invoice Query (Consulta NF-e) + +**Resource:** `nfe.productInvoiceQuery` + +Query NF-e (Nota Fiscal Eletrônica) product invoices directly on SEFAZ by access key. This is a read-only resource that does not require company scope. + +> **Note:** This resource uses a separate API host (`nfe.api.nfe.io`). You can configure a specific API key with `dataApiKey`, or the SDK will use `apiKey` as fallback. + +#### `retrieve(accessKey: string): Promise` + +Retrieve full product invoice details from SEFAZ by access key. + +```typescript +const invoice = await nfe.productInvoiceQuery.retrieve( + '35240112345678000190550010000001231234567890' +); +console.log(invoice.currentStatus); // 'authorized' +console.log(invoice.issuer?.name); +console.log(invoice.totals?.icms?.invoiceAmount); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `accessKey` | `string` | Yes | 44-digit numeric access key (Chave de Acesso) | + +**Returns:** `ProductInvoiceDetails` — Full invoice details including issuer, buyer, items, totals, transport, and payment. + +**Throws:** +- `ValidationError` if access key format is invalid +- `NotFoundError` if no invoice matches the access key (HTTP 404) +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### `downloadPdf(accessKey: string): Promise` + +Download the DANFE PDF for a product invoice. + +```typescript +const pdfBuffer = await nfe.productInvoiceQuery.downloadPdf( + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('danfe.pdf', pdfBuffer); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `accessKey` | `string` | Yes | 44-digit numeric access key | + +**Returns:** `Buffer` containing the PDF binary content. + +#### `downloadXml(accessKey: string): Promise` + +Download the raw NF-e XML for a product invoice. + +```typescript +const xmlBuffer = await nfe.productInvoiceQuery.downloadXml( + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('nfe.xml', xmlBuffer); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `accessKey` | `string` | Yes | 44-digit numeric access key | + +**Returns:** `Buffer` containing the XML binary content. + +#### `listEvents(accessKey: string): Promise` + +List fiscal events (cancellations, corrections, manifestations) for a product invoice. + +```typescript +const result = await nfe.productInvoiceQuery.listEvents( + '35240112345678000190550010000001231234567890' +); +for (const event of result.events ?? []) { + console.log(event.description, event.authorizedOn); +} +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `accessKey` | `string` | Yes | 44-digit numeric access key | + +**Returns:** `ProductInvoiceEventsResponse` with an array of fiscal events and query timestamp. + +--- + +### Consumer Invoice Query (Consulta CFe-SAT) + +**Resource:** `nfe.consumerInvoiceQuery` + +Query CFe-SAT (Cupom Fiscal Eletrônico) consumer invoices by access key. This is a read-only resource that does not require company scope. + +> **Note:** This resource uses a separate API host (`nfe.api.nfe.io`). You can configure a specific API key with `dataApiKey`, or the SDK will use `apiKey` as fallback. + +#### `retrieve(accessKey: string): Promise` + +Retrieve full CFe-SAT coupon details by access key. + +```typescript +const coupon = await nfe.consumerInvoiceQuery.retrieve( + '35240112345678000190590000000012341234567890' +); +console.log(coupon.currentStatus); // 'Authorized' +console.log(coupon.issuer?.name); +console.log(coupon.totals?.couponAmount); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `accessKey` | `string` | Yes | 44-digit numeric access key (Chave de Acesso) | + +**Returns:** `TaxCoupon` — Full coupon details including issuer, buyer, items, totals, and payment. + +**Throws:** +- `ValidationError` if access key format is invalid +- `NotFoundError` if no coupon matches the access key (HTTP 404) +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### `downloadXml(accessKey: string): Promise` + +Download the raw CFe XML for a consumer invoice. + +```typescript +const xmlBuffer = await nfe.consumerInvoiceQuery.downloadXml( + '35240112345678000190590000000012341234567890' +); +fs.writeFileSync('cfe.xml', xmlBuffer); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `accessKey` | `string` | Yes | 44-digit numeric access key | + +**Returns:** `Buffer` containing the XML binary content. + +--- + +### Legal Entity Lookup (Consulta CNPJ) + +**Resource:** `nfe.legalEntityLookup` + +Query Brazilian company (CNPJ) data from Receita Federal and state tax registries (SEFAZ). This is a read-only resource that does not require company scope. + +> **Note:** This resource uses a separate API host (`legalentity.api.nfe.io`). You can configure a specific API key with `dataApiKey`, or the SDK will use `apiKey` as fallback. + +#### `getBasicInfo(federalTaxNumber: string, options?: LegalEntityBasicInfoOptions): Promise` + +Lookup basic company information by CNPJ from Receita Federal. Returns legal name, trade name, address, phone numbers, economic activities (CNAE), legal nature, partners, and registration status. + +```typescript +const result = await nfe.legalEntityLookup.getBasicInfo('12.345.678/0001-90'); +console.log(result.legalEntity?.name); // 'EMPRESA LTDA' +console.log(result.legalEntity?.status); // 'Active' +console.log(result.legalEntity?.address?.city?.name); + +// With options +const result = await nfe.legalEntityLookup.getBasicInfo('12345678000190', { + updateAddress: false, + updateCityCode: true, +}); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `federalTaxNumber` | `string` | Yes | CNPJ, with or without punctuation (e.g., `"12345678000190"` or `"12.345.678/0001-90"`) | +| `options` | `LegalEntityBasicInfoOptions` | No | Lookup options | + +**Options:** + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `updateAddress` | `boolean` | `true` | Update address from postal service data | +| `updateCityCode` | `boolean` | `false` | Update only the city IBGE code when `updateAddress` is `false` | + +**Returns:** `LegalEntityBasicInfoResponse` — Company basic information including address, phones, activities, partners. + +**Throws:** +- `ValidationError` if CNPJ format is invalid (not 14 digits after stripping punctuation) +- `NotFoundError` if no company found for the given CNPJ (HTTP 404) +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### `getStateTaxInfo(state: string, federalTaxNumber: string): Promise` + +Lookup state tax registration (Inscrição Estadual) by CNPJ and state. Returns tax regime, legal nature, and state tax registration details including fiscal document indicators (NFe, NFSe, CTe, NFCe). + +```typescript +const result = await nfe.legalEntityLookup.getStateTaxInfo('SP', '12345678000190'); +console.log(result.legalEntity?.taxRegime); // 'SimplesNacional' + +for (const tax of result.legalEntity?.stateTaxes ?? []) { + console.log(`IE: ${tax.taxNumber} — Status: ${tax.status}`); + console.log(` NFe: ${tax.nfe?.status}, NFSe: ${tax.nfse?.status}`); +} +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `state` | `string` | Yes | Brazilian state code (e.g., `"SP"`, `"RJ"`, `"MG"`). Case-insensitive. | +| `federalTaxNumber` | `string` | Yes | CNPJ, with or without punctuation | + +**Returns:** `LegalEntityStateTaxResponse` — State tax registration information. + +**Throws:** +- `ValidationError` if state code is invalid or CNPJ format is invalid +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### `getStateTaxForInvoice(state: string, federalTaxNumber: string): Promise` + +Evaluate state tax registration for invoice issuance. Returns extended status information (including `UnabledTemp`, `UnabledNotConfirmed`) useful for determining whether product invoices (NF-e) can be issued. + +```typescript +const result = await nfe.legalEntityLookup.getStateTaxForInvoice('MG', '12345678000190'); +for (const tax of result.legalEntity?.stateTaxes ?? []) { + if (tax.status === 'Abled') { + console.log(`Can issue invoices with IE: ${tax.taxNumber}`); + } +} +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `state` | `string` | Yes | Brazilian state code. Case-insensitive. | +| `federalTaxNumber` | `string` | Yes | CNPJ, with or without punctuation | + +**Returns:** `LegalEntityStateTaxForInvoiceResponse` — State tax data with extended status for invoice evaluation. + +**Throws:** +- `ValidationError` if state code is invalid or CNPJ format is invalid +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### `getSuggestedStateTaxForInvoice(state: string, federalTaxNumber: string): Promise` + +Get the best (suggested) state tax registration for invoice issuance. When multiple registrations are enabled in a state, NFE.io applies evaluation criteria to recommend the optimal IE. + +```typescript +const result = await nfe.legalEntityLookup.getSuggestedStateTaxForInvoice('SP', '12345678000190'); +const bestIE = result.legalEntity?.stateTaxes?.[0]; +if (bestIE) { + console.log(`Recommended IE: ${bestIE.taxNumber} (${bestIE.status})`); +} +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `state` | `string` | Yes | Brazilian state code. Case-insensitive. | +| `federalTaxNumber` | `string` | Yes | CNPJ, with or without punctuation | + +**Returns:** `LegalEntityStateTaxForInvoiceResponse` — Suggested state tax data prioritized by NFE.io criteria. + +**Throws:** +- `ValidationError` if state code is invalid or CNPJ format is invalid +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### Types + +```typescript +type BrazilianState = + | 'AC' | 'AL' | 'AM' | 'AP' | 'BA' | 'CE' | 'DF' | 'ES' | 'GO' + | 'MA' | 'MG' | 'MS' | 'MT' | 'PA' | 'PB' | 'PE' | 'PI' | 'PR' + | 'RJ' | 'RN' | 'RO' | 'RR' | 'RS' | 'SC' | 'SE' | 'SP' | 'TO' + | 'EX' | 'NA'; + +interface LegalEntityBasicInfoOptions { + updateAddress?: boolean; + updateCityCode?: boolean; +} + +interface LegalEntityBasicInfoResponse { + legalEntity?: LegalEntityBasicInfo; +} + +interface LegalEntityBasicInfo { + tradeName?: string; + name?: string; + federalTaxNumber?: number; + size?: 'Unknown' | 'ME' | 'EPP' | 'DEMAIS'; + openedOn?: string; + address?: LegalEntityAddress; + phones?: LegalEntityPhone[]; + status?: 'Unknown' | 'Active' | 'Suspended' | 'Cancelled' | 'Unabled' | 'Null'; + email?: string; + shareCapital?: number; + economicActivities?: LegalEntityEconomicActivity[]; + legalNature?: LegalEntityNature; + partners?: LegalEntityPartner[]; + unit?: 'Headoffice' | 'Subsidiary'; + // ... and more fields +} + +interface LegalEntityStateTaxResponse { + legalEntity?: LegalEntityStateTaxInfo; +} + +interface LegalEntityStateTaxForInvoiceResponse { + legalEntity?: LegalEntityStateTaxForInvoiceInfo; +} + +interface LegalEntityStateTax { + status?: 'Abled' | 'Unabled' | 'Cancelled' | 'Unknown'; + taxNumber?: string; + code?: BrazilianState; + nfe?: LegalEntityFiscalDocumentInfo; + nfse?: LegalEntityFiscalDocumentInfo; + cte?: LegalEntityFiscalDocumentInfo; + nfce?: LegalEntityFiscalDocumentInfo; + // ... and more fields +} + +interface LegalEntityStateTaxForInvoice { + status?: 'Abled' | 'Unabled' | 'Cancelled' | 'UnabledTemp' | 'UnabledNotConfirmed' + | 'Unknown' | 'UnknownTemp' | 'UnknownNotConfirmed'; + taxNumber?: string; + // ... same structure as LegalEntityStateTax with extended status +} +``` + +> See [src/core/types.ts](../src/core/types.ts) for the complete type definitions. + +--- + +### Natural Person Lookup (Consulta CPF) + +**Resource:** `nfe.naturalPersonLookup` +**API Host:** `naturalperson.api.nfe.io` +**Authentication:** Uses `dataApiKey` (falls back to `apiKey`) + +Lookup CPF cadastral status (situação cadastral) at the Brazilian Federal Revenue Service (Receita Federal). + +#### `getStatus(federalTaxNumber: string, birthDate: string | Date): Promise` + +Query the cadastral status of a CPF, returning the person's name, CPF, birth date, status, and query timestamp. + +```typescript +// With string date +const result = await nfe.naturalPersonLookup.getStatus('123.456.789-01', '1990-01-15'); +console.log(result.name); // 'JOÃO DA SILVA' +console.log(result.status); // 'Regular' + +// With Date object +const result = await nfe.naturalPersonLookup.getStatus('12345678901', new Date(1990, 0, 15)); +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `federalTaxNumber` | `string` | Yes | CPF number, with or without punctuation (e.g., `"12345678901"` or `"123.456.789-01"`) | +| `birthDate` | `string \| Date` | Yes | Date of birth in `YYYY-MM-DD` format or a `Date` object | + +**Returns:** `NaturalPersonStatusResponse` — CPF cadastral status data. + +**Throws:** +- `ValidationError` if CPF format is invalid (not 11 digits) or birth date format is invalid +- `NotFoundError` if CPF is not found or birth date does not match (HTTP 404) +- `AuthenticationError` if API key is invalid (HTTP 401) + +#### Types + +```typescript +type NaturalPersonStatus = + | 'Regular' + | 'Suspensa' + | 'Cancelada' + | 'Titular Falecido' + | 'Pendente de Regularização' + | 'Nula' + | (string & {}); + +interface NaturalPersonStatusResponse { + name?: string; + federalTaxNumber: string; + birthOn?: string; + status?: NaturalPersonStatus; + createdOn?: string; +} +``` + +> See [src/core/types.ts](../src/core/types.ts) for the complete type definitions. + +--- + +### Tax Calculation (Cálculo de Impostos) + +**Resource:** `nfe.taxCalculation` +**API Host:** `api.nfse.io` +**Authentication:** Uses `dataApiKey` (falls back to `apiKey`) + +Compute all applicable Brazilian taxes (ICMS, ICMS-ST, PIS, COFINS, IPI, II) for product operations using the Tax Calculation Engine (Motor de Cálculo de Tributos). + +#### `calculate(tenantId: string, request: CalculateRequest): Promise` + +Submit an operation with issuer, recipient, operation type, and product items to compute per-item tax breakdowns. + +```typescript +const result = await nfe.taxCalculation.calculate('tenant-id', { + operationType: 'Outgoing', + issuer: { state: 'SP', taxRegime: 'RealProfit' }, + recipient: { state: 'RJ' }, + items: [{ + id: 'item-1', + operationCode: 121, + origin: 'National', + ncm: '61091000', + quantity: 10, + unitAmount: 100.00 + }] +}); + +for (const item of result.items ?? []) { + console.log(`Item ${item.id}: CFOP ${item.cfop}`); + console.log(` ICMS CST: ${item.icms?.cst}, value: ${item.icms?.vICMS}`); +} +``` + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `tenantId` | `string` | Yes | Subscription/account ID that scopes the tax rules | +| `request` | `CalculateRequest` | Yes | Tax calculation request payload | + +**Returns:** `CalculateResponse` — Per-item tax breakdowns including CFOP, ICMS, PIS, COFINS, IPI, II. + +**Throws:** +- `ValidationError` if `tenantId` is empty +- `ValidationError` if required fields are missing (issuer, recipient, operationType, items) +- `AuthenticationError` if API key is invalid (HTTP 401) +- `BadRequestError` if the API rejects the payload (HTTP 400) + +#### Types + +```typescript +type TaxOperationType = 'Outgoing' | 'Incoming'; + +type TaxOrigin = + | 'National' | 'ForeignDirectImport' | 'ForeignInternalMarket' + | 'NationalWith40To70Import' | 'NationalPpb' | 'NationalWithLess40Import' + | 'ForeignDirectImportWithoutNationalSimilar' + | 'ForeignInternalMarketWithoutNationalSimilar' + | 'NationalWithGreater70Import'; + +type TaxCalcTaxRegime = + | 'NationalSimple' | 'RealProfit' | 'PresumedProfit' + | 'NationalSimpleSublimitExceeded' | 'IndividualMicroEnterprise' | 'Exempt'; + +interface CalculateRequest { + collectionId?: string; + issuer: CalculateRequestIssuer; // required: state, taxRegime + recipient: CalculateRequestRecipient; // required: state + operationType: TaxOperationType; + items: CalculateItemRequest[]; // required: id, operationCode, origin, quantity, unitAmount + isProductRegistration?: boolean; +} + +interface CalculateResponse { + items?: CalculateItemResponse[]; // per-item: cfop, icms, pis, cofins, ipi, ii, icmsUfDest +} +``` + +> See [src/core/types.ts](../src/core/types.ts) for the complete type definitions including all tax component interfaces (TaxIcms, TaxPis, TaxCofins, TaxIpi, TaxIi, TaxIcmsUfDest). + +--- + +### Tax Codes (Códigos Auxiliares) + +**Resource:** `nfe.taxCodes` +**API Host:** `api.nfse.io` +**Authentication:** Uses `dataApiKey` (falls back to `apiKey`) + +Paginated listings of auxiliary tax code reference tables needed as inputs for the Tax Calculation Engine. + +#### `listOperationCodes(options?: TaxCodeListOptions): Promise` + +List operation codes (natureza de operação) — e.g., 121 = "Venda de mercadoria". + +```typescript +const result = await nfe.taxCodes.listOperationCodes({ pageIndex: 1, pageCount: 20 }); +console.log(`Total: ${result.totalCount}, Page ${result.currentPage} of ${result.totalPages}`); +for (const code of result.items ?? []) { + console.log(`${code.code} - ${code.description}`); +} +``` + +#### `listAcquisitionPurposes(options?: TaxCodeListOptions): Promise` + +List acquisition purposes (finalidade de aquisição). + +#### `listIssuerTaxProfiles(options?: TaxCodeListOptions): Promise` + +List issuer tax profiles (perfil fiscal do emissor). + +#### `listRecipientTaxProfiles(options?: TaxCodeListOptions): Promise` + +List recipient tax profiles (perfil fiscal do destinatário). + +**All methods accept:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `options.pageIndex` | `number` | No | Page index, 1-based (default: 1) | +| `options.pageCount` | `number` | No | Items per page (default: 50) | + +**Returns:** `TaxCodePaginatedResponse` — Paginated list of tax codes. + +#### Types + +```typescript +interface TaxCode { + code?: string; + description?: string; +} + +interface TaxCodePaginatedResponse { + items?: TaxCode[]; + currentPage?: number; + totalPages?: number; + totalCount?: number; +} + +interface TaxCodeListOptions { + pageIndex?: number; + pageCount?: number; +} +``` + +> See [src/core/types.ts](../src/core/types.ts) for the complete type definitions. + +--- + +### Product Invoices (NF-e Emissão) + +**Resource:** `nfe.productInvoices` +**API Host:** `api.nfse.io` +**Authentication:** Uses `dataApiKey` (falls back to `apiKey`) + +Full lifecycle management for NF-e (Nota Fiscal Eletrônica de Produto) product invoices — issue, list, retrieve, cancel, send correction letters (CC-e), disable invoice numbers, and download files (PDF/XML). + +> **Important:** Issue, cancel, correction letter, and disablement operations are asynchronous — they return 202/204 indicating the request was enqueued. Completion is notified via webhooks. + +#### `create(companyId, data): Promise` + +Issue a product invoice (NF-e) by posting it to the processing queue. + +```typescript +const result = await nfe.productInvoices.create('company-id', { + operationNature: 'Venda de mercadoria', + operationType: 'Outgoing', + buyer: { name: 'Empresa LTDA', federalTaxNumber: 12345678000190 }, + items: [{ code: 'PROD-001', description: 'Produto X', quantity: 1, unitAmount: 100 }], + payment: [{ paymentDetail: [{ method: 'Cash', amount: 100 }] }], +}); +``` + +#### `createWithStateTax(companyId, stateTaxId, data): Promise` + +Issue an NF-e specifying a particular state tax registration (Inscrição Estadual). + +```typescript +const result = await nfe.productInvoices.createWithStateTax('company-id', 'state-tax-id', invoiceData); +``` + +#### `list(companyId, options): Promise` + +List product invoices with cursor-based pagination. The `environment` option is **required**. + +```typescript +const invoices = await nfe.productInvoices.list('company-id', { + environment: 'Production', // Required: 'Production' or 'Test' + limit: 10, // Optional (default: 10) + startingAfter: 'cursor-id', // Optional: cursor-based pagination + q: "buyer.name:'EMPRESA'", // Optional: ElasticSearch query +}); +for (const inv of invoices.productInvoices ?? []) { + console.log(inv.id, inv.status); +} +``` + +#### `retrieve(companyId, invoiceId): Promise` + +Retrieve full details of a single NF-e invoice. + +```typescript +const invoice = await nfe.productInvoices.retrieve('company-id', 'invoice-id'); +console.log(invoice.status, invoice.authorization?.accessKey); +``` + +#### `cancel(companyId, invoiceId, reason?): Promise` + +Cancel a product invoice (asynchronous — enqueues for cancellation). + +```typescript +const result = await nfe.productInvoices.cancel('company-id', 'invoice-id', 'Erro de digitação'); +``` + +#### `listItems(companyId, invoiceId, options?): Promise` + +List items (products/services) for a specific invoice. + +```typescript +const items = await nfe.productInvoices.listItems('company-id', 'invoice-id', { limit: 20 }); +``` + +#### `listEvents(companyId, invoiceId, options?): Promise` + +List fiscal events for a specific invoice. + +```typescript +const events = await nfe.productInvoices.listEvents('company-id', 'invoice-id'); +``` + +#### `downloadPdf(companyId, invoiceId, force?): Promise` + +Get the URL for the DANFE PDF file. + +```typescript +const pdf = await nfe.productInvoices.downloadPdf('company-id', 'invoice-id'); +console.log('PDF URL:', pdf.uri); + +// Force regeneration +const pdfForced = await nfe.productInvoices.downloadPdf('company-id', 'invoice-id', true); +``` + +#### `downloadXml(companyId, invoiceId): Promise` + +Get the URL for the authorized NF-e XML file. + +```typescript +const xml = await nfe.productInvoices.downloadXml('company-id', 'invoice-id'); +console.log('XML URL:', xml.uri); +``` + +#### `downloadRejectionXml(companyId, invoiceId): Promise` + +Get the URL for the NF-e rejection XML (uses `/xml-rejection` canonical path). + +#### `downloadEpecXml(companyId, invoiceId): Promise` + +Get the URL for the contingency authorization (EPEC) XML. + +#### `sendCorrectionLetter(companyId, invoiceId, reason): Promise` + +Send a correction letter (Carta de Correção — CC-e). The reason must be 15–1,000 characters. + +```typescript +const result = await nfe.productInvoices.sendCorrectionLetter( + 'company-id', + 'invoice-id', + 'Correcao do endereco do destinatario conforme novo cadastro' +); +``` + +#### `downloadCorrectionLetterPdf(companyId, invoiceId): Promise` + +Get the URL for the CC-e DANFE PDF. + +#### `downloadCorrectionLetterXml(companyId, invoiceId): Promise` + +Get the URL for the CC-e XML. + +#### `disable(companyId, invoiceId, reason?): Promise` + +Disable (inutilizar) a specific product invoice by ID. + +```typescript +await nfe.productInvoices.disable('company-id', 'invoice-id', 'Numeração inutilizada'); +``` + +#### `disableRange(companyId, data): Promise` + +Disable a range of invoice numbers. + +```typescript +const result = await nfe.productInvoices.disableRange('company-id', { + environment: 'Production', + serie: 1, + state: 'SP', + beginNumber: 100, + lastNumber: 110, + reason: 'Faixa de numeração inutilizada', +}); +``` + +**Parameters common to sub-list methods (`listItems`, `listEvents`):** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `options.limit` | `number` | No | Items per page (default: 10) | +| `options.startingAfter` | `number` | No | Cursor for pagination | + +--- + +### State Taxes (Inscrições Estaduais) + +**Resource:** `nfe.stateTaxes` +**API Host:** `api.nfse.io` +**Authentication:** Uses `dataApiKey` (falls back to `apiKey`) + +CRUD operations for company state tax registrations (Inscrições Estaduais). State taxes define the series, numbering, environment, and state code configuration required for NF-e issuance. + +#### `list(companyId, options?): Promise` + +List all state tax registrations for a company. + +```typescript +const result = await nfe.stateTaxes.list('company-id'); +for (const tax of result.stateTaxes ?? []) { + console.log(tax.id, tax.taxNumber, tax.code, tax.serie, tax.status); +} +``` + +#### `create(companyId, data): Promise` + +Create a new state tax registration. Body is automatically wrapped as `{ stateTax: data }`. + +```typescript +const tax = await nfe.stateTaxes.create('company-id', { + taxNumber: '123456789', + serie: 1, + number: 1, + code: 'sP', + environmentType: 'production', + type: 'nFe', +}); +console.log('Created:', tax.id); +``` + +#### `retrieve(companyId, stateTaxId): Promise` + +Retrieve a specific state tax registration by ID. + +```typescript +const tax = await nfe.stateTaxes.retrieve('company-id', 'state-tax-id'); +``` + +#### `update(companyId, stateTaxId, data): Promise` + +Update an existing state tax registration. Body is automatically wrapped as `{ stateTax: data }`. + +```typescript +const updated = await nfe.stateTaxes.update('company-id', 'state-tax-id', { + serie: 2, + environmentType: 'test', +}); +``` + +#### `delete(companyId, stateTaxId): Promise` + +Delete a state tax registration. + +```typescript +await nfe.stateTaxes.delete('company-id', 'state-tax-id'); +``` + +#### Types + +```typescript +type NfeStateTaxType = 'default' | 'nFe' | 'nFCe'; +type NfeStateTaxEnvironmentType = 'none' | 'production' | 'test'; +type NfeStateTaxStatus = 'inactive' | 'none' | 'active'; + +interface NfeStateTax { + id?: string; + companyId?: string; + accountId?: string; + code?: NfeStateTaxStateCode; + environmentType?: NfeStateTaxEnvironmentType; + taxNumber?: string; + serie?: number; + number?: number; + status?: NfeStateTaxStatus; + specialTaxRegime?: string; + securityCredential?: NfeStateTaxSecurityCredential; + type?: NfeStateTaxType; + series?: NfeStateTaxSeries[]; + batchId?: string; + createdOn?: string; + modifiedOn?: string; +} +``` + +> See [src/core/types.ts](../src/core/types.ts) for the complete type definitions. + +--- + ## Types ### Core Types @@ -1511,6 +2715,7 @@ const events = await nfe.webhooks.getAvailableEvents(); ```typescript interface NfeConfig { apiKey?: string; + dataApiKey?: string; // API key for data/query services (Addresses, CT-e, CNPJ, CPF) environment?: 'production' | 'development'; baseUrl?: string; timeout?: number; @@ -1781,7 +2986,12 @@ import type { NaturalPerson, Webhook, ListResponse, - PaginationOptions + PaginationOptions, + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings, + EnableInboundOptions, + ManifestEventType } from 'nfe-io'; const config: NfeConfig = { diff --git a/examples/README.md b/examples/README.md index a47559e..0313a6a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -165,6 +165,18 @@ Demonstra todos os recursos disponíveis no SDK. ### **jsdoc-intellisense-demo.ts** - IntelliSense Demo Demonstra autocompletar e tipos do editor. +### **tax-calculation.js** - Cálculo de Impostos 🧮 +Demonstra o Motor de Cálculo de Tributos para operações com produtos: +- ✅ Listar códigos de operação e perfis fiscais disponíveis +- ✅ Enviar requisição de cálculo de impostos +- ✅ Inspecionar detalhamento por item (ICMS, PIS, COFINS, IPI, II) + +```bash +node examples/tax-calculation.js +``` + +**Requer**: `NFE_TENANT_ID` no `.env.test` com o ID da subscription/conta. + --- ## 📖 Ordem Recomendada de Execução diff --git a/examples/address-lookup.js b/examples/address-lookup.js index fa4f650..0e11743 100644 --- a/examples/address-lookup.js +++ b/examples/address-lookup.js @@ -5,7 +5,7 @@ * Brazilian addresses (CEP/postal code lookups). * * Prerequisites: - * - Set NFE_ADDRESS_API_KEY or NFE_API_KEY environment variable + * - Set NFE_DATA_API_KEY or NFE_API_KEY environment variable * - Or pass the API key directly in the configuration * * Run this example: @@ -14,12 +14,12 @@ import { NfeClient } from '../dist/index.js'; -// Configuration with separate address API key (optional) +// Configuration with separate data API key (optional) const client = new NfeClient({ - // You can use a separate API key for address lookups - // addressApiKey: process.env.NFE_ADDRESS_API_KEY, + // You can use a separate API key for data/query services (addresses, CT-e) + // dataApiKey: process.env.NFE_DATA_API_KEY, - // Or use the main API key (will be used as fallback for addresses) + // Or use the main API key (will be used as fallback for data services) apiKey: process.env.NFE_API_KEY, // Environment: 'production' or 'development' @@ -103,21 +103,21 @@ async function searchWithFilter() { } /** - * Example 4: Using only addressApiKey (isolated usage) + * Example 4: Using only dataApiKey (isolated usage) */ async function isolatedAddressUsage() { - console.log('\n🔐 Example 4: Isolated Address API Usage'); + console.log('\n🔐 Example 4: Isolated Data API Usage'); console.log('='.repeat(50)); - // Create a client with ONLY addressApiKey - // This is useful when you only have access to the Address API - const addressOnlyClient = new NfeClient({ - addressApiKey: process.env.NFE_ADDRESS_API_KEY || process.env.NFE_API_KEY, + // Create a client with ONLY dataApiKey + // This is useful when you only have access to data/query services + const dataOnlyClient = new NfeClient({ + dataApiKey: process.env.NFE_DATA_API_KEY || process.env.NFE_API_KEY, }); try { // Addresses work - const result = await addressOnlyClient.addresses.lookupByPostalCode('20040-020'); + const result = await dataOnlyClient.addresses.lookupByPostalCode('20040-020'); console.log('Rio de Janeiro CEP lookup succeeded!'); console.log(` ${result.street}, ${result.city?.name}/${result.state}`); } catch (error) { @@ -165,9 +165,9 @@ async function main() { console.log('🏠 NFE.io Address Lookup Examples'); console.log('━'.repeat(50)); - if (!process.env.NFE_API_KEY && !process.env.NFE_ADDRESS_API_KEY) { + if (!process.env.NFE_API_KEY && !process.env.NFE_DATA_API_KEY) { console.error('\n❌ No API key found!'); - console.error('Please set NFE_API_KEY or NFE_ADDRESS_API_KEY environment variable.'); + console.error('Please set NFE_API_KEY or NFE_DATA_API_KEY environment variable.'); console.error('\nExample:'); console.error(' export NFE_API_KEY="your-api-key"'); console.error(' node examples/address-lookup.js'); diff --git a/examples/cnpj-lookup.js b/examples/cnpj-lookup.js new file mode 100644 index 0000000..560b804 --- /dev/null +++ b/examples/cnpj-lookup.js @@ -0,0 +1,255 @@ +/** + * NFE.io SDK v3 - CNPJ / Legal Entity Lookup Examples + * + * This example demonstrates how to use the Legal Entity Lookup API for querying + * Brazilian company (CNPJ) data including basic info, state tax registrations, + * and invoice-readiness evaluation. + * + * Prerequisites: + * - Set NFE_DATA_API_KEY or NFE_API_KEY environment variable + * - Or pass the API key directly in the configuration + * + * Run this example: + * node examples/cnpj-lookup.js + */ + +import { NfeClient } from '../dist/index.js'; + +// Configuration +const client = new NfeClient({ + // The Legal Entity API uses dataApiKey (falls back to apiKey) + // dataApiKey: process.env.NFE_DATA_API_KEY, + apiKey: process.env.NFE_API_KEY, + environment: 'production', +}); + +// Example CNPJ — replace with a real one for testing +const EXAMPLE_CNPJ = process.env.TEST_CNPJ || '12345678000190'; +const EXAMPLE_STATE = process.env.TEST_STATE || 'SP'; + +/** + * Example 1: Basic CNPJ lookup + */ +async function basicLookup() { + console.log('\n🏢 Example 1: Basic CNPJ Lookup'); + console.log('='.repeat(50)); + + try { + const result = await client.legalEntityLookup.getBasicInfo(EXAMPLE_CNPJ); + + const entity = result.legalEntity; + if (entity) { + console.log('Legal Name:', entity.name); + console.log('Trade Name:', entity.tradeName); + console.log('CNPJ:', entity.federalTaxNumber); + console.log('Status:', entity.status); + console.log('Size:', entity.size); + console.log('Opened On:', entity.openedOn); + console.log('Unit:', entity.unit); + console.log('Share Capital:', entity.shareCapital); + console.log('Email:', entity.email); + + if (entity.address) { + console.log('\nAddress:'); + console.log(` ${entity.address.street}, ${entity.address.number}`); + console.log(` ${entity.address.district}`); + console.log(` ${entity.address.city?.name}/${entity.address.state}`); + console.log(` CEP: ${entity.address.postalCode}`); + } + + if (entity.phones?.length) { + console.log('\nPhones:'); + for (const phone of entity.phones) { + console.log(` (${phone.ddd}) ${phone.number}`); + } + } + + if (entity.economicActivities?.length) { + console.log('\nEconomic Activities (CNAE):'); + for (const activity of entity.economicActivities.slice(0, 5)) { + console.log(` [${activity.type}] ${activity.code} - ${activity.description}`); + } + } + + if (entity.legalNature) { + console.log('\nLegal Nature:', `${entity.legalNature.code} - ${entity.legalNature.description}`); + } + + if (entity.partners?.length) { + console.log('\nPartners:'); + for (const partner of entity.partners) { + console.log(` ${partner.name} (${partner.qualification?.description})`); + } + } + } else { + console.log('No data returned for this CNPJ'); + } + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 2: Lookup with formatted CNPJ and options + */ +async function lookupWithOptions() { + console.log('\n⚙️ Example 2: Lookup with Options'); + console.log('='.repeat(50)); + + try { + // Formatted CNPJ is accepted — punctuation is stripped automatically + const result = await client.legalEntityLookup.getBasicInfo('12.345.678/0001-90', { + updateAddress: false, // Skip postal service address enrichment + updateCityCode: true, // But still update city IBGE code + }); + + console.log('Lookup with options succeeded'); + console.log('Entity name:', result.legalEntity?.name ?? 'N/A'); + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 3: State tax registration lookup (Inscrição Estadual) + */ +async function stateTaxLookup() { + console.log('\n📋 Example 3: State Tax Registration Lookup'); + console.log('='.repeat(50)); + + try { + const result = await client.legalEntityLookup.getStateTaxInfo(EXAMPLE_STATE, EXAMPLE_CNPJ); + + const entity = result.legalEntity; + if (entity) { + console.log('Legal Name:', entity.name); + console.log('Tax Regime:', entity.taxRegime); + console.log('Legal Nature:', entity.legalNature); + console.log('Fiscal Unit:', entity.fiscalUnit); + console.log('Check Code:', entity.checkCode); + + if (entity.stateTaxes?.length) { + console.log('\nState Tax Registrations:'); + for (const tax of entity.stateTaxes) { + console.log(` IE: ${tax.taxNumber}`); + console.log(` Status: ${tax.status}`); + console.log(` State: ${tax.code}`); + console.log(` Opened: ${tax.openedOn}`); + if (tax.nfe) console.log(` NFe: ${tax.nfe.status}`); + if (tax.nfse) console.log(` NFSe: ${tax.nfse.status}`); + if (tax.cte) console.log(` CTe: ${tax.cte.status}`); + if (tax.nfce) console.log(` NFCe: ${tax.nfce.status}`); + console.log(); + } + } else { + console.log('No state tax registrations found'); + } + } + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 4: State tax evaluation for invoice issuance + */ +async function stateTaxForInvoice() { + console.log('\n📄 Example 4: State Tax for Invoice Evaluation'); + console.log('='.repeat(50)); + + try { + const result = await client.legalEntityLookup.getStateTaxForInvoice(EXAMPLE_STATE, EXAMPLE_CNPJ); + + const taxes = result.legalEntity?.stateTaxes ?? []; + console.log(`Found ${taxes.length} state tax registration(s)`); + + for (const tax of taxes) { + const canIssue = tax.status === 'Abled'; + console.log(` IE: ${tax.taxNumber} — ${tax.status} ${canIssue ? '✅' : '❌'}`); + } + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 5: Suggested (best) state tax for invoice issuance + */ +async function suggestedStateTax() { + console.log('\n⭐ Example 5: Suggested State Tax for Invoice'); + console.log('='.repeat(50)); + + try { + const result = await client.legalEntityLookup.getSuggestedStateTaxForInvoice(EXAMPLE_STATE, EXAMPLE_CNPJ); + + const bestTax = result.legalEntity?.stateTaxes?.[0]; + if (bestTax) { + console.log('Recommended IE:', bestTax.taxNumber); + console.log('Status:', bestTax.status); + console.log('State:', bestTax.code); + if (bestTax.nfe) console.log('NFe Status:', bestTax.nfe.status); + } else { + console.log('No suggested state tax registration available'); + } + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 6: Error handling + */ +async function errorHandling() { + console.log('\n⚠️ Example 6: Error Handling'); + console.log('='.repeat(50)); + + // Invalid CNPJ format + try { + await client.legalEntityLookup.getBasicInfo('123'); + } catch (error) { + console.log('ValidationError for invalid CNPJ:', error.message); + } + + // Invalid state code + try { + await client.legalEntityLookup.getStateTaxInfo('XX', EXAMPLE_CNPJ); + } catch (error) { + console.log('ValidationError for invalid state:', error.message); + } + + // Empty CNPJ + try { + await client.legalEntityLookup.getBasicInfo(''); + } catch (error) { + console.log('ValidationError for empty CNPJ:', error.message); + } +} + +/** + * Main function to run all examples + */ +async function main() { + console.log('🏢 NFE.io CNPJ / Legal Entity Lookup Examples'); + console.log('━'.repeat(50)); + + if (!process.env.NFE_API_KEY && !process.env.NFE_DATA_API_KEY) { + console.error('\n❌ No API key found!'); + console.error('Please set NFE_API_KEY or NFE_DATA_API_KEY environment variable.'); + console.error('\nExample:'); + console.error(' export NFE_API_KEY="your-api-key"'); + console.error(' node examples/cnpj-lookup.js'); + process.exit(1); + } + + await basicLookup(); + await lookupWithOptions(); + await stateTaxLookup(); + await stateTaxForInvoice(); + await suggestedStateTax(); + await errorHandling(); + + console.log('\n✅ All examples completed!'); +} + +// Run main function +main().catch(console.error); diff --git a/examples/consumer-invoice-query.js b/examples/consumer-invoice-query.js new file mode 100644 index 0000000..982f51e --- /dev/null +++ b/examples/consumer-invoice-query.js @@ -0,0 +1,91 @@ +/** + * Consumer Invoice Query Example (CFe-SAT) + * + * Demonstrates querying CFe-SAT (Cupom Fiscal Eletrônico) consumer invoices + * by access key using the NFE.io SDK v3. + * + * Setup: + * export NFE_DATA_API_KEY="your-data-api-key" + * # or: export NFE_API_KEY="your-api-key" + * + * Usage: + * node examples/consumer-invoice-query.js + */ + +import { NfeClient } from '../src/index.js'; + +// Replace with a real CFe-SAT access key (44 digits) +const ACCESS_KEY = process.argv[2] || '35240112345678000190590000000012341234567890'; + +async function main() { + const nfe = new NfeClient({ + apiKey: process.env.NFE_API_KEY, + dataApiKey: process.env.NFE_DATA_API_KEY, + }); + + console.log('=== Consumer Invoice Query (CFe-SAT) ===\n'); + + // 1. Retrieve coupon details + try { + console.log(`Retrieving coupon: ${ACCESS_KEY}`); + const coupon = await nfe.consumerInvoiceQuery.retrieve(ACCESS_KEY); + + console.log('\n--- Coupon Details ---'); + console.log('Status:', coupon.currentStatus); + console.log('Number:', coupon.number); + console.log('SAT Serie:', coupon.satSerie); + console.log('Issued On:', coupon.issuedOn); + console.log('Access Key:', coupon.accessKey); + + if (coupon.issuer) { + console.log('\n--- Issuer ---'); + console.log('Name:', coupon.issuer.name); + console.log('Trade Name:', coupon.issuer.tradeName); + console.log('CNPJ:', coupon.issuer.federalTaxNumber); + console.log('Tax Regime:', coupon.issuer.taxRegime); + } + + if (coupon.buyer) { + console.log('\n--- Buyer ---'); + console.log('Name:', coupon.buyer.name); + console.log('CPF/CNPJ:', coupon.buyer.federalTaxNumber); + } + + if (coupon.totals) { + console.log('\n--- Totals ---'); + console.log('Coupon Amount:', coupon.totals.couponAmount); + console.log('Total Tax (approx):', coupon.totals.totalAmount); + } + + if (coupon.items && coupon.items.length > 0) { + console.log(`\n--- Items (${coupon.items.length}) ---`); + for (const item of coupon.items) { + console.log(` ${item.code} - ${item.description}: ${item.quantity} x ${item.unitAmount} = ${item.netAmount}`); + } + } + + if (coupon.payment?.paymentDetails) { + console.log('\n--- Payment ---'); + for (const detail of coupon.payment.paymentDetails) { + console.log(` ${detail.method}: ${detail.amount}`); + } + if (coupon.payment.payBack) { + console.log(' Change:', coupon.payment.payBack); + } + } + } catch (error) { + console.error('Failed to retrieve coupon:', error instanceof Error ? error.message : error); + } + + // 2. Download CFe XML + try { + console.log(`\nDownloading CFe XML for: ${ACCESS_KEY}`); + const xmlBuffer = await nfe.consumerInvoiceQuery.downloadXml(ACCESS_KEY); + console.log(`XML downloaded: ${xmlBuffer.length} bytes`); + // fs.writeFileSync('cfe.xml', xmlBuffer); + } catch (error) { + console.error('Failed to download XML:', error instanceof Error ? error.message : error); + } +} + +main().catch(console.error); diff --git a/examples/cpf-lookup.js b/examples/cpf-lookup.js new file mode 100644 index 0000000..5d91648 --- /dev/null +++ b/examples/cpf-lookup.js @@ -0,0 +1,117 @@ +/** + * NFE.io SDK v3 - CPF / Natural Person Lookup Example + * + * This example demonstrates how to use the Natural Person Lookup API for querying + * Brazilian CPF cadastral status (situação cadastral) at Receita Federal. + * + * Prerequisites: + * - Set NFE_DATA_API_KEY or NFE_API_KEY environment variable + * - Or pass the API key directly in the configuration + * + * Run this example: + * node examples/cpf-lookup.js + */ + +import { NfeClient } from '../dist/index.js'; + +// Configuration +const client = new NfeClient({ + // The Natural Person API uses dataApiKey (falls back to apiKey) + // dataApiKey: process.env.NFE_DATA_API_KEY, + apiKey: process.env.NFE_API_KEY, + environment: 'production', +}); + +// Example CPF and birth date — replace with real values for testing +const EXAMPLE_CPF = process.env.TEST_CPF || '12345678901'; +const EXAMPLE_BIRTH_DATE = process.env.TEST_BIRTH_DATE || '1990-01-15'; + +/** + * Example 1: CPF lookup with string date + */ +async function cpfLookupWithString() { + console.log('\n👤 Example 1: CPF Lookup with String Date'); + console.log('='.repeat(50)); + + try { + const result = await client.naturalPersonLookup.getStatus(EXAMPLE_CPF, EXAMPLE_BIRTH_DATE); + + console.log('Name: ', result.name ?? 'N/A'); + console.log('CPF: ', result.federalTaxNumber); + console.log('Birth Date: ', result.birthOn ?? 'N/A'); + console.log('Cadastral Status: ', result.status ?? 'N/A'); + console.log('Query Timestamp: ', result.createdOn ?? 'N/A'); + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 2: CPF lookup with formatted CPF and Date object + */ +async function cpfLookupWithDate() { + console.log('\n👤 Example 2: CPF Lookup with Date Object'); + console.log('='.repeat(50)); + + try { + // The SDK accepts formatted CPF (punctuation is stripped automatically) + const formattedCpf = '123.456.789-01'; + + // The SDK also accepts Date objects for birth date + const birthDate = new Date(1990, 0, 15); // January 15, 1990 + + const result = await client.naturalPersonLookup.getStatus(formattedCpf, birthDate); + + console.log('Name: ', result.name ?? 'N/A'); + console.log('Cadastral Status: ', result.status ?? 'N/A'); + } catch (error) { + console.error('Error:', error.message); + } +} + +/** + * Example 3: Error handling + */ +async function errorHandling() { + console.log('\n⚠️ Example 3: Error Handling'); + console.log('='.repeat(50)); + + // Invalid CPF (too short) + try { + await client.naturalPersonLookup.getStatus('123', '1990-01-15'); + } catch (error) { + console.log('Validation error (short CPF):', error.message); + } + + // Invalid birth date format + try { + await client.naturalPersonLookup.getStatus('12345678901', '15/01/1990'); + } catch (error) { + console.log('Validation error (bad date):', error.message); + } + + // CPF not found (404) + try { + await client.naturalPersonLookup.getStatus('00000000000', '2000-01-01'); + } catch (error) { + console.log('API error (not found): ', error.message); + } +} + +/** + * Run all examples + */ +async function main() { + console.log('🇧🇷 NFE.io SDK v3 - CPF Lookup Examples'); + console.log('━'.repeat(50)); + console.log(`Using CPF: ${EXAMPLE_CPF}`); + console.log(`Using birth date: ${EXAMPLE_BIRTH_DATE}`); + + await cpfLookupWithString(); + await cpfLookupWithDate(); + await errorHandling(); + + console.log('\n✅ All examples completed'); +} + +main().catch(console.error); diff --git a/examples/inbound-product-invoices.js b/examples/inbound-product-invoices.js new file mode 100644 index 0000000..9019a50 --- /dev/null +++ b/examples/inbound-product-invoices.js @@ -0,0 +1,258 @@ +/** + * NFE.io SDK v3 - Inbound Product Invoices (NF-e Distribuição) Example + * + * This example demonstrates how to use the Inbound Product Invoices API + * for querying NF-e documents received via Distribuição NF-e (DF-e). + * + * Prerequisites: + * - Company must be registered with a valid A1 digital certificate + * - Webhook must be configured to receive NF-e notifications + * - Valid API key with NF-e distribution access + * + * Configuration: + * Set one of the following environment variables: + * - NFE_DATA_API_KEY - Data/query API key (recommended) + * - NFE_API_KEY - Main API key (will be used as fallback) + * + * Usage: + * node inbound-product-invoices.js [accessKey] + * + * Examples: + * node inbound-product-invoices.js 12345 # Enable and check settings + * node inbound-product-invoices.js 12345 35240... # Retrieve specific NF-e + */ + +import { NfeClient } from 'nfe-io'; + +// ============================================================================ +// Configuration +// ============================================================================ + +const nfe = new NfeClient({ + // dataApiKey: process.env.NFE_DATA_API_KEY, // Uncomment for explicit configuration +}); + +// ============================================================================ +// Example Functions +// ============================================================================ + +/** + * Enable automatic NF-e inbound fetch for a company + */ +async function enableAutoFetch(companyId) { + console.log('\n📡 Enabling automatic NF-e inbound fetch...'); + + try { + const settings = await nfe.inboundProductInvoices.enableAutoFetch(companyId, { + environmentSEFAZ: 'Production', + webhookVersion: '2', + }); + + console.log('✅ Auto-fetch enabled!'); + console.log(' Status:', settings.status); + console.log(' Environment:', settings.environmentSEFAZ); + console.log(' Webhook version:', settings.webhookVersion); + if (settings.startFromNsu) { + console.log(' Starting from NSU:', settings.startFromNsu); + } + return settings; + } catch (error) { + console.error('❌ Failed to enable auto-fetch:', error.message); + throw error; + } +} + +/** + * Get current inbound settings + */ +async function getSettings(companyId) { + console.log('\n⚙️ Fetching inbound settings...'); + + try { + const settings = await nfe.inboundProductInvoices.getSettings(companyId); + console.log('📋 Current settings:'); + console.log(' Status:', settings.status); + console.log(' Environment:', settings.environmentSEFAZ ?? 'Not set'); + console.log(' Webhook version:', settings.webhookVersion); + console.log(' Start from NSU:', settings.startFromNsu ?? 'Not set'); + console.log(' Created:', settings.createdOn); + console.log(' Modified:', settings.modifiedOn); + return settings; + } catch (error) { + console.error('❌ Failed to get settings:', error.message); + throw error; + } +} + +/** + * Get NF-e details by access key (webhook v2 format) + */ +async function getInvoiceDetails(companyId, accessKey) { + console.log('\n🔍 Fetching NF-e details (webhook v2)...'); + + try { + const invoice = await nfe.inboundProductInvoices.getProductInvoiceDetails( + companyId, + accessKey + ); + + console.log('📄 Invoice details:'); + console.log(' Access Key:', invoice.accessKey); + console.log(' NSU:', invoice.nsu); + console.log(' NF-e Number:', invoice.nfeNumber); + console.log(' Issuer:', invoice.issuer?.name); + console.log(' Buyer:', invoice.buyer?.name); + console.log(' Amount:', invoice.totalInvoiceAmount); + console.log(' Issued on:', invoice.issuedOn); + if (invoice.productInvoices?.length) { + console.log(' Product invoices:', invoice.productInvoices.length); + } + return invoice; + } catch (error) { + console.error('❌ Failed to get invoice details:', error.message); + throw error; + } +} + +/** + * Download invoice XML + */ +async function downloadXml(companyId, accessKey) { + console.log('\n📥 Downloading NF-e XML...'); + + try { + const xml = await nfe.inboundProductInvoices.getXml(companyId, accessKey); + console.log('✅ XML downloaded successfully'); + console.log(' Size:', xml.length, 'characters'); + // In a real app, you'd save to file: + // fs.writeFileSync(`nfe-${accessKey}.xml`, xml); + return xml; + } catch (error) { + console.error('❌ Failed to download XML:', error.message); + throw error; + } +} + +/** + * Download invoice PDF (DANFE) + */ +async function downloadPdf(companyId, accessKey) { + console.log('\n📥 Downloading NF-e PDF (DANFE)...'); + + try { + const pdf = await nfe.inboundProductInvoices.getPdf(companyId, accessKey); + console.log('✅ PDF downloaded successfully'); + // In a real app, you'd save to file: + // fs.writeFileSync(`nfe-${accessKey}.pdf`, pdf); + return pdf; + } catch (error) { + console.error('❌ Failed to download PDF:', error.message); + throw error; + } +} + +/** + * Send manifest event (Ciência da Operação by default) + */ +async function sendManifest(companyId, accessKey, tpEvent = 210210) { + const eventNames = { + 210210: 'Ciência da Operação', + 210220: 'Confirmação da Operação', + 210240: 'Operação não Realizada', + }; + const eventName = eventNames[tpEvent] || `Event ${tpEvent}`; + + console.log(`\n📨 Sending manifest: ${eventName}...`); + + try { + const result = await nfe.inboundProductInvoices.manifest( + companyId, + accessKey, + tpEvent + ); + console.log('✅ Manifest sent successfully'); + return result; + } catch (error) { + console.error('❌ Failed to send manifest:', error.message); + throw error; + } +} + +/** + * Reprocess a webhook notification + */ +async function reprocessWebhook(companyId, accessKeyOrNsu) { + console.log('\n🔄 Reprocessing webhook...'); + + try { + const result = await nfe.inboundProductInvoices.reprocessWebhook( + companyId, + accessKeyOrNsu + ); + console.log('✅ Webhook reprocessed successfully'); + return result; + } catch (error) { + console.error('❌ Failed to reprocess webhook:', error.message); + throw error; + } +} + +/** + * Disable automatic NF-e fetching + */ +async function disableAutoFetch(companyId) { + console.log('\n🔌 Disabling auto-fetch...'); + + try { + const settings = await nfe.inboundProductInvoices.disableAutoFetch(companyId); + console.log('✅ Auto-fetch disabled'); + console.log(' Status:', settings.status); + return settings; + } catch (error) { + console.error('❌ Failed to disable auto-fetch:', error.message); + throw error; + } +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + const companyId = process.argv[2]; + const accessKey = process.argv[3]; + + if (!companyId) { + console.error('Usage: node inbound-product-invoices.js [accessKey]'); + process.exit(1); + } + + console.log('🏢 Company ID:', companyId); + if (accessKey) { + console.log('🔑 Access Key:', accessKey); + } + + // Step 1: Enable auto-fetch and check settings + await enableAutoFetch(companyId); + await getSettings(companyId); + + // Step 2: If access key provided, fetch details and downloads + if (accessKey) { + await getInvoiceDetails(companyId, accessKey); + await downloadXml(companyId, accessKey); + await downloadPdf(companyId, accessKey); + + // Step 3: Send manifest (Ciência da Operação) + await sendManifest(companyId, accessKey); + + // Step 4: Reprocess webhook + await reprocessWebhook(companyId, accessKey); + } + + console.log('\n✨ Done!'); +} + +main().catch((error) => { + console.error('\n💥 Unhandled error:', error); + process.exit(1); +}); diff --git a/examples/product-invoice-query.js b/examples/product-invoice-query.js new file mode 100644 index 0000000..0552162 --- /dev/null +++ b/examples/product-invoice-query.js @@ -0,0 +1,163 @@ +/** + * Exemplo - Consulta de NF-e por Chave de Acesso + * + * Este exemplo demonstra como usar o recurso productInvoiceQuery para: + * - Consultar dados completos de uma NF-e na SEFAZ + * - Baixar DANFE (PDF) + * - Baixar XML da NF-e + * - Listar eventos fiscais (cancelamentos, correções, etc.) + * + * Pré-requisitos: + * - Chave API configurada (apiKey ou dataApiKey) + * - Chave de acesso válida de 44 dígitos + * + * Execute: node examples/product-invoice-query.js + */ + +import { NfeClient } from '../dist/index.js'; +import * as dotenv from 'dotenv'; +import { writeFileSync } from 'fs'; + +// Carregar credenciais do .env.test +dotenv.config({ path: '.env.test' }); + +const apiKey = process.env.NFE_API_KEY || process.env.NFE_DATA_API_KEY; + +if (!apiKey) { + console.error('❌ NFE_API_KEY ou NFE_DATA_API_KEY não encontrada no .env.test'); + process.exit(1); +} + +// Chave de acesso de exemplo (substitua por uma chave real para testar) +const ACCESS_KEY = process.argv[2] || '35240112345678000190550010000001231234567890'; + +// Inicializar cliente +const nfe = new NfeClient({ + apiKey, + environment: process.env.NFE_TEST_ENVIRONMENT || 'production', +}); + +console.log('🔍 NFE.io SDK v3 - Consulta de NF-e por Chave de Acesso'); +console.log('═'.repeat(70)); +console.log(`Chave de acesso: ${ACCESS_KEY}`); +console.log('═'.repeat(70)); + +// ============================================================================ +// 1. Consultar dados completos da NF-e +// ============================================================================ + +async function retrieveInvoice() { + console.log('\n📋 1. Consultando dados da NF-e...'); + + try { + const invoice = await nfe.productInvoiceQuery.retrieve(ACCESS_KEY); + + console.log(` Status: ${invoice.currentStatus}`); + console.log(` Emissor: ${invoice.issuer?.name ?? 'N/A'}`); + console.log(` CNPJ Emissor: ${invoice.issuer?.federalTaxNumber ?? 'N/A'}`); + console.log(` Destinatário: ${invoice.buyer?.name ?? 'N/A'}`); + + if (invoice.totals?.icms) { + console.log(` Valor Total: R$ ${invoice.totals.icms.invoiceAmount?.toFixed(2) ?? 'N/A'}`); + console.log(` Base ICMS: R$ ${invoice.totals.icms.taxBaseAmount?.toFixed(2) ?? 'N/A'}`); + console.log(` Valor ICMS: R$ ${invoice.totals.icms.taxAmount?.toFixed(2) ?? 'N/A'}`); + } + + if (invoice.items?.length) { + console.log(` Itens: ${invoice.items.length}`); + for (const item of invoice.items.slice(0, 3)) { + console.log(` - ${item.description} (Qtd: ${item.quantity})`); + } + if (invoice.items.length > 3) { + console.log(` ... e mais ${invoice.items.length - 3} itens`); + } + } + + return invoice; + } catch (err) { + console.error(` ❌ Erro: ${err.message}`); + return null; + } +} + +// ============================================================================ +// 2. Baixar DANFE (PDF) +// ============================================================================ + +async function downloadPdf() { + console.log('\n📄 2. Baixando DANFE (PDF)...'); + + try { + const pdfBuffer = await nfe.productInvoiceQuery.downloadPdf(ACCESS_KEY); + const filename = `danfe-${ACCESS_KEY.substring(0, 10)}.pdf`; + writeFileSync(filename, pdfBuffer); + console.log(` ✅ DANFE salvo em: ${filename} (${pdfBuffer.length} bytes)`); + } catch (err) { + console.error(` ❌ Erro: ${err.message}`); + } +} + +// ============================================================================ +// 3. Baixar XML da NF-e +// ============================================================================ + +async function downloadXml() { + console.log('\n📝 3. Baixando XML da NF-e...'); + + try { + const xmlBuffer = await nfe.productInvoiceQuery.downloadXml(ACCESS_KEY); + const filename = `nfe-${ACCESS_KEY.substring(0, 10)}.xml`; + writeFileSync(filename, xmlBuffer); + console.log(` ✅ XML salvo em: ${filename} (${xmlBuffer.length} bytes)`); + } catch (err) { + console.error(` ❌ Erro: ${err.message}`); + } +} + +// ============================================================================ +// 4. Listar eventos fiscais +// ============================================================================ + +async function listEvents() { + console.log('\n📅 4. Listando eventos fiscais...'); + + try { + const result = await nfe.productInvoiceQuery.listEvents(ACCESS_KEY); + + if (!result.events?.length) { + console.log(' Nenhum evento encontrado'); + return; + } + + console.log(` Eventos encontrados: ${result.events.length}`); + for (const event of result.events) { + console.log(` - ${event.eventType}: ${event.description}`); + console.log(` Protocolo: ${event.protocol ?? 'N/A'}`); + console.log(` Data: ${event.authorizedOn ?? 'N/A'}`); + } + console.log(` Consultado em: ${result.queriedAt}`); + } catch (err) { + console.error(` ❌ Erro: ${err.message}`); + } +} + +// ============================================================================ +// Executar todos os exemplos +// ============================================================================ + +async function main() { + try { + await retrieveInvoice(); + await downloadPdf(); + await downloadXml(); + await listEvents(); + + console.log('\n' + '═'.repeat(70)); + console.log('✅ Exemplo concluído!'); + } catch (err) { + console.error('\n❌ Erro inesperado:', err.message); + process.exit(1); + } +} + +main(); diff --git a/examples/product-invoices.js b/examples/product-invoices.js new file mode 100644 index 0000000..1ae369c --- /dev/null +++ b/examples/product-invoices.js @@ -0,0 +1,128 @@ +/** + * NFE.io SDK v3 — Product Invoices (NF-e) Example + * + * Demonstrates creating, listing, retrieving, downloading files, + * sending correction letters, and disabling product invoices. + * + * Usage: + * NFE_APIKEY=your-key node examples/product-invoices.js + */ + +const { NfeClient } = require('../src/index.js'); + +const apiKey = process.env.NFE_APIKEY || process.env.NFE_API_KEY; +if (!apiKey) { + console.error('Set NFE_APIKEY or NFE_API_KEY environment variable'); + process.exit(1); +} + +const companyId = process.env.NFE_COMPANY_ID || 'YOUR_COMPANY_ID'; + +const nfe = new NfeClient({ + apiKey, + environment: 'sandbox', +}); + +async function main() { + // ── 1. List existing product invoices ───────────────────────────────────── + console.log('\n=== List Product Invoices (Test environment) ==='); + const list = await nfe.productInvoices.list(companyId, { + environment: 'Test', + limit: 5, + }); + console.log(`Found ${list.totalCount ?? 0} invoices, showing ${list.productInvoices?.length ?? 0}`); + for (const inv of list.productInvoices ?? []) { + console.log(` ${inv.id} — status: ${inv.status}`); + } + + // ── 2. Issue a product invoice ──────────────────────────────────────────── + console.log('\n=== Issue Product Invoice ==='); + const issueData = { + operationNature: 'Venda de mercadoria', + operationType: 'Outgoing', + buyer: { + name: 'Empresa Exemplo LTDA', + federalTaxNumber: 12345678000190, + address: { + street: 'Rua Exemplo', + number: '100', + district: 'Centro', + city: { code: '3550308', name: 'São Paulo' }, + state: 'SP', + country: 'Brasil', + postalCode: '01001000', + }, + }, + items: [ + { + code: 'PROD-001', + description: 'Produto de Teste', + quantity: 2, + unitAmount: 50.0, + ncmCode: '84713019', + }, + ], + payment: [ + { + paymentDetail: [{ method: 'Cash', amount: 100.0 }], + }, + ], + }; + + try { + const created = await nfe.productInvoices.create(companyId, issueData); + console.log('Invoice enqueued (202):', JSON.stringify(created, null, 2).slice(0, 200)); + } catch (err) { + console.error('Issue failed:', err.message); + } + + // ── 3. Retrieve a specific invoice ──────────────────────────────────────── + if (list.productInvoices?.length) { + const invoiceId = list.productInvoices[0].id; + console.log(`\n=== Retrieve Invoice ${invoiceId} ===`); + const invoice = await nfe.productInvoices.retrieve(companyId, invoiceId); + console.log(` Status: ${invoice.status}`); + console.log(` Access Key: ${invoice.authorization?.accessKey ?? 'pending'}`); + + // ── 4. Download PDF ─────────────────────────────────────────────────── + console.log('\n=== Download DANFE PDF ==='); + try { + const pdf = await nfe.productInvoices.downloadPdf(companyId, invoiceId); + console.log(' PDF URI:', pdf.uri); + } catch (err) { + console.log(' PDF not available:', err.message); + } + + // ── 5. Download XML ─────────────────────────────────────────────────── + console.log('\n=== Download XML ==='); + try { + const xml = await nfe.productInvoices.downloadXml(companyId, invoiceId); + console.log(' XML URI:', xml.uri); + } catch (err) { + console.log(' XML not available:', err.message); + } + + // ── 6. List items ───────────────────────────────────────────────────── + console.log('\n=== List Invoice Items ==='); + const items = await nfe.productInvoices.listItems(companyId, invoiceId); + console.log(` ${items.totalCount ?? 0} items`); + + // ── 7. List events ──────────────────────────────────────────────────── + console.log('\n=== List Invoice Events ==='); + const events = await nfe.productInvoices.listEvents(companyId, invoiceId); + console.log(` ${events.totalCount ?? 0} events`); + } + + // ── 8. State Taxes (Inscrições Estaduais) ─────────────────────────────── + console.log('\n=== List State Taxes ==='); + const taxes = await nfe.stateTaxes.list(companyId); + console.log(`Found ${taxes.totalCount ?? taxes.stateTaxes?.length ?? 0} registrations`); + for (const tax of taxes.stateTaxes ?? []) { + console.log(` ${tax.id} — ${tax.taxNumber} (${tax.code}, serie ${tax.serie})`); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/examples/setup.js b/examples/setup.js index 2a7a4d7..a0f5707 100644 --- a/examples/setup.js +++ b/examples/setup.js @@ -112,6 +112,10 @@ ${companyId ? `NFE_COMPANY_ID=${companyId}` : '# NFE_COMPANY_ID=seu-company-id-a # Timeout em ms (opcional) # NFE_TIMEOUT=30000 + +# Chave de API para serviços de consulta (opcional) +# Se não definida, usa NFE_API_KEY como fallback +# NFE_DATA_API_KEY=sua-chave-data-aqui `; try { diff --git a/examples/state-taxes.js b/examples/state-taxes.js new file mode 100644 index 0000000..0f5836e --- /dev/null +++ b/examples/state-taxes.js @@ -0,0 +1,85 @@ +/** + * NFE.io SDK v3 — State Taxes (Inscrições Estaduais) Example + * + * Demonstrates CRUD operations for company state tax registrations + * (Inscrições Estaduais) — required configuration for NF-e product + * invoice issuance. + * + * Usage: + * NFE_APIKEY=your-key node examples/state-taxes.js + */ + +const { NfeClient } = require('../src/index.js'); + +const apiKey = process.env.NFE_APIKEY || process.env.NFE_API_KEY; +if (!apiKey) { + console.error('Set NFE_APIKEY or NFE_API_KEY environment variable'); + process.exit(1); +} + +const companyId = process.env.NFE_COMPANY_ID || 'YOUR_COMPANY_ID'; + +const nfe = new NfeClient({ + apiKey, + environment: 'sandbox', +}); + +async function main() { + // ── 1. List existing state tax registrations ───────────────────────────── + console.log('\n=== List State Taxes ==='); + const list = await nfe.stateTaxes.list(companyId); + console.log(`Found ${list.stateTaxes?.length ?? 0} registrations`); + for (const tax of list.stateTaxes ?? []) { + console.log(` ${tax.id} — IE: ${tax.taxNumber}, state: ${tax.code}, serie: ${tax.serie}, status: ${tax.status}`); + } + + // ── 2. List with pagination ────────────────────────────────────────────── + console.log('\n=== List with Pagination ==='); + const paginated = await nfe.stateTaxes.list(companyId, { limit: 2 }); + console.log(`Showing ${paginated.stateTaxes?.length ?? 0} registrations`); + + // ── 3. Create a state tax registration ─────────────────────────────────── + console.log('\n=== Create State Tax Registration ==='); + try { + const created = await nfe.stateTaxes.create(companyId, { + taxNumber: '123456789', + serie: 1, + number: 1, + code: 'sP', + environmentType: 'test', + type: 'nFe', + }); + console.log('Created:', created.id, '— IE:', created.taxNumber); + + // ── 4. Retrieve the created registration ───────────────────────────── + console.log('\n=== Retrieve State Tax ==='); + const retrieved = await nfe.stateTaxes.retrieve(companyId, created.id); + console.log(` ID: ${retrieved.id}`); + console.log(` Tax Number: ${retrieved.taxNumber}`); + console.log(` State: ${retrieved.code}`); + console.log(` Serie: ${retrieved.serie}`); + console.log(` Number: ${retrieved.number}`); + console.log(` Environment: ${retrieved.environmentType}`); + console.log(` Status: ${retrieved.status}`); + + // ── 5. Update the registration ─────────────────────────────────────── + console.log('\n=== Update State Tax ==='); + const updated = await nfe.stateTaxes.update(companyId, created.id, { + serie: 2, + environmentType: 'production', + }); + console.log(`Updated serie to ${updated.serie}, environment to ${updated.environmentType}`); + + // ── 6. Delete the registration ─────────────────────────────────────── + console.log('\n=== Delete State Tax ==='); + await nfe.stateTaxes.delete(companyId, created.id); + console.log('Deleted successfully'); + } catch (err) { + console.error('Operation failed:', err.message); + } +} + +main().catch(err => { + console.error('Error:', err.message); + process.exit(1); +}); diff --git a/examples/tax-calculation.js b/examples/tax-calculation.js new file mode 100644 index 0000000..8c7fbcf --- /dev/null +++ b/examples/tax-calculation.js @@ -0,0 +1,165 @@ +/** + * NFE.io SDK v3 - Tax Calculation Example + * + * Demonstrates the standalone tax calculation flow: + * 1. List available operation codes and tax profiles + * 2. Submit a tax calculation request + * 3. Inspect the per-item tax breakdown + * + * Prerequisites: + * - Configure .env.test with NFE_API_KEY (or NFE_DATA_API_KEY) + * - npm run build + * + * Run: + * node examples/tax-calculation.js + */ + +// Load environment variables +const path = require('path'); +const fs = require('fs'); + +const envPath = path.resolve(__dirname, '..', '.env.test'); +if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf-8'); + for (const line of envContent.split('\n')) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const eqIndex = trimmed.indexOf('='); + if (eqIndex > 0) { + const key = trimmed.substring(0, eqIndex).trim(); + const value = trimmed.substring(eqIndex + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } + } + } +} + +const { NfeClient } = require('../dist/index.js'); + +async function main() { + console.log('='.repeat(60)); + console.log(' NFE.io SDK v3 - Tax Calculation Example'); + console.log('='.repeat(60)); + console.log(); + + const apiKey = process.env.NFE_DATA_API_KEY || process.env.NFE_API_KEY; + if (!apiKey) { + console.error('❌ API key not found. Set NFE_API_KEY or NFE_DATA_API_KEY in .env.test'); + process.exit(1); + } + + const nfe = new NfeClient({ + apiKey: apiKey, + environment: process.env.NFE_TEST_ENVIRONMENT || 'development', + }); + + // ----------------------------------------------------------------------- + // 1. List available reference codes (optional — for discovery) + // ----------------------------------------------------------------------- + console.log('📋 Step 1: Listing available tax codes...\n'); + + try { + const operationCodes = await nfe.taxCodes.listOperationCodes({ pageIndex: 1, pageCount: 5 }); + console.log(` Operation codes: ${operationCodes.totalCount ?? '?'} total`); + for (const code of (operationCodes.items ?? []).slice(0, 3)) { + console.log(` ${code.code} - ${code.description}`); + } + console.log(); + + const issuerProfiles = await nfe.taxCodes.listIssuerTaxProfiles({ pageIndex: 1, pageCount: 5 }); + console.log(` Issuer tax profiles: ${issuerProfiles.totalCount ?? '?'} total`); + for (const profile of (issuerProfiles.items ?? []).slice(0, 3)) { + console.log(` ${profile.code} - ${profile.description}`); + } + console.log(); + } catch (err) { + console.log(` ⚠️ Could not list tax codes: ${err.message}`); + console.log(' (This is expected if the API host is not reachable or credentials differ)\n'); + } + + // ----------------------------------------------------------------------- + // 2. Submit a tax calculation request + // ----------------------------------------------------------------------- + console.log('🧮 Step 2: Calculating taxes for a sample operation...\n'); + + // NOTE: Replace 'your-tenant-id' with your actual subscription/account ID + const tenantId = process.env.NFE_TENANT_ID || 'your-tenant-id'; + + const calculateRequest = { + operationType: 'Outgoing', // Sale / Saída + issuer: { + state: 'SP', + taxRegime: 'RealProfit', + }, + recipient: { + state: 'RJ', + }, + items: [ + { + id: 'item-1', + operationCode: 121, // Venda de mercadoria (example) + origin: 'National', + ncm: '61091000', // T-shirts, knitted + quantity: 10, + unitAmount: 100.00, + freightAmount: 50.00, + }, + { + id: 'item-2', + operationCode: 121, + origin: 'National', + ncm: '39174090', // Plastic tubes + quantity: 50, + unitAmount: 25.50, + discountAmount: 10.00, + }, + ], + }; + + try { + const result = await nfe.taxCalculation.calculate(tenantId, calculateRequest); + + console.log(' ✅ Tax calculation successful!\n'); + console.log(` Items calculated: ${result.items?.length ?? 0}\n`); + + for (const item of result.items ?? []) { + console.log(` 📦 Item ${item.id}:`); + console.log(` CFOP: ${item.cfop}`); + if (item.icms) { + console.log(` ICMS: CST=${item.icms.cst || item.icms.csosn}, ` + + `Base=${item.icms.vBC}, Rate=${item.icms.pICMS}%, Value=${item.icms.vICMS}`); + } + if (item.pis) { + console.log(` PIS: CST=${item.pis.cst}, Value=${item.pis.vPIS}`); + } + if (item.cofins) { + console.log(` COFINS: CST=${item.cofins.cst}, Value=${item.cofins.vCOFINS}`); + } + if (item.ipi) { + console.log(` IPI: CST=${item.ipi.cst}, Value=${item.ipi.vIPI}`); + } + if (item.icmsUfDest) { + console.log(` ICMS UF Dest: ${item.icmsUfDest.vICMSUFDest}`); + } + if (item.benefit) { + console.log(` Benefit: ${item.benefit}`); + } + console.log(); + } + } catch (err) { + console.log(` ❌ Tax calculation failed: ${err.message}`); + if (tenantId === 'your-tenant-id') { + console.log(' 💡 Set NFE_TENANT_ID in .env.test with your subscription ID'); + } + console.log(); + } + + console.log('='.repeat(60)); + console.log(' Example complete!'); + console.log('='.repeat(60)); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/examples/transportation-invoices.js b/examples/transportation-invoices.js new file mode 100644 index 0000000..232f245 --- /dev/null +++ b/examples/transportation-invoices.js @@ -0,0 +1,289 @@ +/** + * NFE.io SDK v3 - Transportation Invoices (CT-e) Example + * + * This example demonstrates how to use the Transportation Invoices (CT-e) API + * for managing Conhecimento de Transporte Eletrônico documents via Distribuição DFe. + * + * Prerequisites: + * - Company must be registered with a valid A1 digital certificate + * - Webhook must be configured to receive CT-e notifications + * - Valid CT-e API key (or main API key with CT-e access) + * + * Configuration: + * Set one of the following environment variables: + * - NFE_DATA_API_KEY - Data/query API key (recommended) + * - NFE_API_KEY - Main API key (will be used as fallback) + * + * Or configure in code: + * const nfe = new NfeClient({ + * dataApiKey: 'your-data-api-key', // Or use apiKey if you have unified access + * }); + * + * Usage: + * node transportation-invoices.js [accessKey] + * + * Examples: + * node transportation-invoices.js 12345 # Enable and check settings + * node transportation-invoices.js 12345 35240... # Retrieve specific CT-e + */ + +import { NfeClient } from 'nfe-io'; + +// ============================================================================ +// Configuration +// ============================================================================ + +// Create client - API key fallback chain: +// 1. dataApiKey (config) +// 2. apiKey (config) +// 3. NFE_DATA_API_KEY (env) +// 4. NFE_API_KEY (env) +const nfe = new NfeClient({ + // dataApiKey: process.env.NFE_DATA_API_KEY, // Uncomment for explicit configuration +}); + +// ============================================================================ +// Example Functions +// ============================================================================ + +/** + * Enable automatic CT-e search for a company + */ +async function enableAutomaticSearch(companyId) { + console.log('\n📡 Enabling automatic CT-e search...'); + + try { + // Enable with default settings + const settings = await nfe.transportationInvoices.enable(companyId); + + console.log('✅ Automatic search enabled!'); + console.log(' Status:', settings.status); + console.log(' Start from NSU:', settings.startFromNsu); + console.log(' Created:', settings.createdOn); + + return settings; + } catch (error) { + if (error.name === 'BadRequestError') { + console.log('⚠️ Already enabled or invalid request:', error.message); + } else { + throw error; + } + } +} + +/** + * Enable automatic CT-e search starting from a specific NSU + */ +async function enableFromNsu(companyId, startFromNsu) { + console.log(`\n📡 Enabling CT-e search starting from NSU ${startFromNsu}...`); + + const settings = await nfe.transportationInvoices.enable(companyId, { + startFromNsu: startFromNsu + }); + + console.log('✅ Enabled with custom NSU!'); + console.log(' Start from NSU:', settings.startFromNsu); + + return settings; +} + +/** + * Enable automatic CT-e search starting from a specific date + */ +async function enableFromDate(companyId, startDate) { + console.log(`\n📡 Enabling CT-e search starting from ${startDate}...`); + + const settings = await nfe.transportationInvoices.enable(companyId, { + startFromDate: startDate + }); + + console.log('✅ Enabled with custom date!'); + console.log(' Start from date:', settings.startFromDate); + + return settings; +} + +/** + * Get current automatic search settings + */ +async function getSettings(companyId) { + console.log('\n⚙️ Getting current CT-e settings...'); + + try { + const settings = await nfe.transportationInvoices.getSettings(companyId); + + console.log('📋 Current settings:'); + console.log(' Status:', settings.status); + console.log(' Start from NSU:', settings.startFromNsu); + console.log(' Start from date:', settings.startFromDate || 'N/A'); + console.log(' Created:', settings.createdOn); + console.log(' Modified:', settings.modifiedOn); + + return settings; + } catch (error) { + if (error.name === 'NotFoundError') { + console.log('ℹ️ Automatic search not configured for this company'); + } else { + throw error; + } + } +} + +/** + * Disable automatic CT-e search + */ +async function disableAutomaticSearch(companyId) { + console.log('\n🔒 Disabling automatic CT-e search...'); + + try { + const settings = await nfe.transportationInvoices.disable(companyId); + + console.log('✅ Automatic search disabled!'); + console.log(' Status:', settings.status); + + return settings; + } catch (error) { + if (error.name === 'NotFoundError') { + console.log('ℹ️ Automatic search was not enabled'); + } else { + throw error; + } + } +} + +/** + * Retrieve CT-e metadata by access key + */ +async function retrieveCte(companyId, accessKey) { + console.log('\n📄 Retrieving CT-e metadata...'); + console.log(' Access Key:', accessKey); + + const cte = await nfe.transportationInvoices.retrieve(companyId, accessKey); + + console.log('\n📋 CT-e Information:'); + console.log(' Type:', cte.type); + console.log(' Status:', cte.status); + console.log(' Sender:', cte.nameSender); + console.log(' Sender CNPJ:', cte.federalTaxNumberSender); + console.log(' Total Amount:', cte.totalInvoiceAmount ? `R$ ${cte.totalInvoiceAmount.toFixed(2)}` : 'N/A'); + console.log(' Issued:', cte.issuedOn); + console.log(' Received:', cte.receivedOn); + + return cte; +} + +/** + * Download CT-e XML content + */ +async function downloadXml(companyId, accessKey) { + console.log('\n📥 Downloading CT-e XML...'); + + const xml = await nfe.transportationInvoices.downloadXml(companyId, accessKey); + + console.log('✅ XML downloaded successfully!'); + console.log(' Size:', xml.length, 'bytes'); + console.log(' Preview:', xml.substring(0, 100) + '...'); + + // In a real application, you would save this to a file: + // import { writeFileSync } from 'fs'; + // writeFileSync(`cte-${accessKey}.xml`, xml); + + return xml; +} + +/** + * Retrieve event metadata for a CT-e + */ +async function getEvent(companyId, accessKey, eventKey) { + console.log('\n📌 Retrieving CT-e event...'); + console.log(' Access Key:', accessKey); + console.log(' Event Key:', eventKey); + + const event = await nfe.transportationInvoices.getEvent(companyId, accessKey, eventKey); + + console.log('\n📋 Event Information:'); + console.log(' Type:', event.type); + console.log(' Status:', event.status); + + return event; +} + +/** + * Download event XML content + */ +async function downloadEventXml(companyId, accessKey, eventKey) { + console.log('\n📥 Downloading event XML...'); + + const xml = await nfe.transportationInvoices.downloadEventXml(companyId, accessKey, eventKey); + + console.log('✅ Event XML downloaded successfully!'); + console.log(' Size:', xml.length, 'bytes'); + + return xml; +} + +// ============================================================================ +// Main Execution +// ============================================================================ + +async function main() { + const args = process.argv.slice(2); + + if (args.length < 1) { + console.log('Usage: node transportation-invoices.js [accessKey] [eventKey]'); + console.log(''); + console.log('Examples:'); + console.log(' node transportation-invoices.js 12345'); + console.log(' node transportation-invoices.js 12345 35240112345678000190570010000001231234567890'); + console.log(' node transportation-invoices.js 12345 35240112345678000190570010000001231234567890 event-123'); + process.exit(1); + } + + const [companyId, accessKey, eventKey] = args; + + console.log('═══════════════════════════════════════════════════════════════'); + console.log(' NFE.io SDK v3 - Transportation Invoices (CT-e) Demo'); + console.log('═══════════════════════════════════════════════════════════════'); + console.log(' Company ID:', companyId); + if (accessKey) console.log(' Access Key:', accessKey); + if (eventKey) console.log(' Event Key:', eventKey); + console.log('═══════════════════════════════════════════════════════════════'); + + try { + // If access key provided, retrieve the specific CT-e + if (accessKey) { + await retrieveCte(companyId, accessKey); + await downloadXml(companyId, accessKey); + + if (eventKey) { + await getEvent(companyId, accessKey, eventKey); + await downloadEventXml(companyId, accessKey, eventKey); + } + } else { + // Otherwise, demonstrate automatic search management + await getSettings(companyId); + + // Uncomment to enable/disable automatic search: + // await enableAutomaticSearch(companyId); + // await enableFromNsu(companyId, 12345); + // await enableFromDate(companyId, '2024-01-01T00:00:00Z'); + // await disableAutomaticSearch(companyId); + } + + console.log('\n═══════════════════════════════════════════════════════════════'); + console.log(' ✅ Demo completed successfully!'); + console.log('═══════════════════════════════════════════════════════════════'); + + } catch (error) { + console.error('\n❌ Error:', error.message); + if (error.name === 'ConfigurationError') { + console.error(' Make sure you have set NFE_DATA_API_KEY or NFE_API_KEY'); + } + if (error.name === 'ValidationError') { + console.error(' Check your input parameters'); + } + process.exit(1); + } +} + +main(); diff --git a/openapi/spec/calculo-impostos-v1.yaml b/openapi/spec/calculo-impostos-v1.yaml index 69ef913..baee25c 100644 --- a/openapi/spec/calculo-impostos-v1.yaml +++ b/openapi/spec/calculo-impostos-v1.yaml @@ -3,6 +3,7 @@ info: title: Cálculo de Impostos description: "# Introdução\r\n\r\nSeja bem-vindo a documentação da API de Cálculo de Impostos!\r\nNossa API foi criada utilizando o padrão REST que possibilita a integração de seu sistema ao nosso, sendo assim você também pode extender ou recriar as funcionalidades existentes na nossa plataforma, tudo isso consumindo a API que está documentada abaixo.\r\n\r\n# Como usar a API?\r\nLogo a seguir você encontrará todos os recursos e métodos suportados pela API, sendo que essa página possibilita que você teste os recursos e métodos diretamente através dela.\r\n\r\n# Autenticação\r\nVocê precisa de uma chave de API (API Key) para identificar a conta que está realizando solicitações para a API.\r\nPara isso você deve colocar sua chave de API no campo que se encontra no topo desta página para que os métodos funcionem corretamente.\r\nNo seu código de integração temos suporte para autenticação de diversas formas sendo eles:\r\nHTTP Header (Authorization ou X-NFEIO-APIKEY) ou HTTP Query String (api_key) nos dois modos passando o valor da sua chave de api (API Key)." version: v1 +host: https://api.nfse.io paths: /tax-codes/operation-code: get: diff --git a/openapi/spec/cpf-api.yaml b/openapi/spec/consulta-cpf.yaml similarity index 100% rename from openapi/spec/cpf-api.yaml rename to openapi/spec/consulta-cpf.yaml diff --git a/package.json b/package.json index 869d3a0..3000ae5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nfe-io", - "version": "v3.0.2", + "version": "v3.1.0", "description": "Official NFE.io SDK for Node.js - TypeScript native with zero runtime dependencies", "keywords": [ "nfe", diff --git a/src/core/client.ts b/src/core/client.ts index 34f7b01..5338bb1 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -27,9 +27,35 @@ import { NaturalPeopleResource, WebhooksResource, AddressesResource, - ADDRESS_API_BASE_URL + TransportationInvoicesResource, + InboundProductInvoicesResource, + ProductInvoiceQueryResource, + ConsumerInvoiceQueryResource, + LegalEntityLookupResource, + NaturalPersonLookupResource, + TaxCalculationResource, + TaxCodesResource, + ProductInvoicesResource, + StateTaxesResource, + ADDRESS_API_BASE_URL, + NFE_QUERY_API_BASE_URL, + LEGAL_ENTITY_API_BASE_URL, + NATURAL_PERSON_API_BASE_URL } from './resources/index.js'; +// ============================================================================ +// Constants +// ============================================================================ + +/** Base URL for CT-e API (Transportation Invoices) */ +export const CTE_API_BASE_URL = 'https://api.nfse.io'; + +/** Base URL for Legal Entity API (CNPJ Lookup) */ +export { LEGAL_ENTITY_API_BASE_URL } from './resources/index.js'; + +/** Base URL for Natural Person API (CPF Lookup) */ +export { NATURAL_PERSON_API_BASE_URL } from './resources/index.js'; + // ============================================================================ // Main NFE.io Client // ============================================================================ @@ -113,6 +139,18 @@ export class NfeClient { /** @internal HTTP client for address API requests (created lazily) */ private _addressHttp: HttpClient | undefined; + /** @internal HTTP client for CT-e API requests (created lazily) */ + private _cteHttp: HttpClient | undefined; + + /** @internal HTTP client for NF-e query API requests (created lazily) */ + private _nfeQueryHttp: HttpClient | undefined; + + /** @internal HTTP client for Legal Entity API requests (created lazily) */ + private _legalEntityHttp: HttpClient | undefined; + + /** @internal HTTP client for Natural Person API requests (created lazily) */ + private _naturalPersonHttp: HttpClient | undefined; + /** @internal Normalized client configuration */ private readonly config: RequiredNfeConfig; @@ -123,6 +161,16 @@ export class NfeClient { private _naturalPeople: NaturalPeopleResource | undefined; private _webhooks: WebhooksResource | undefined; private _addresses: AddressesResource | undefined; + private _transportationInvoices: TransportationInvoicesResource | undefined; + private _inboundProductInvoices: InboundProductInvoicesResource | undefined; + private _productInvoiceQuery: ProductInvoiceQueryResource | undefined; + private _consumerInvoiceQuery: ConsumerInvoiceQueryResource | undefined; + private _legalEntityLookup: LegalEntityLookupResource | undefined; + private _naturalPersonLookup: NaturalPersonLookupResource | undefined; + private _taxCalculation: TaxCalculationResource | undefined; + private _taxCodes: TaxCodesResource | undefined; + private _productInvoices: ProductInvoicesResource | undefined; + private _stateTaxes: StateTaxesResource | undefined; /** * Service Invoices API resource @@ -273,10 +321,10 @@ export class NfeClient { * - Search by generic term * * **Note:** This resource uses a different API host (address.api.nfe.io). - * Configure `addressApiKey` for a separate key, or it will fallback to `apiKey`. + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. * * @see {@link AddressesResource} - * @throws {ConfigurationError} If no API key is configured (addressApiKey or apiKey) + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) * * @example * ```typescript @@ -291,6 +339,351 @@ export class NfeClient { return this._addresses; } + /** + * Transportation Invoices (CT-e) API resource + * + * @description + * Provides operations for managing CT-e (Conhecimento de Transporte Eletrônico) + * documents via SEFAZ Distribuição DFe: + * - Enable/disable automatic CT-e search + * - Retrieve CT-e metadata and XML + * - Retrieve CT-e event metadata and XML + * + * **Prerequisites:** + * - Company must have a valid A1 digital certificate + * - Webhook must be configured to receive CT-e notifications + * + * **Note:** This resource uses a different API host (api.nfse.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link TransportationInvoicesResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * // Enable automatic CT-e search + * await nfe.transportationInvoices.enable('company-id'); + * + * // Retrieve CT-e metadata + * const cte = await nfe.transportationInvoices.retrieve( + * 'company-id', + * '35240112345678000190570010000001231234567890' + * ); + * ``` + */ + get transportationInvoices(): TransportationInvoicesResource { + if (!this._transportationInvoices) { + this._transportationInvoices = new TransportationInvoicesResource(this.getCteHttpClient()); + } + return this._transportationInvoices; + } + + /** + * Inbound Product Invoices (NF-e Distribution) API resource + * + * @description + * Provides operations for querying NF-e documents received by a company + * via SEFAZ Distribuição DFe: + * - Enable/disable automatic NF-e distribution fetch + * - Retrieve inbound NF-e metadata by access key + * - Download NF-e documents in XML, PDF, and JSON formats + * - Send recipient manifest (Manifestação do Destinatário) + * - Reprocess webhooks + * + * **Prerequisites:** + * - Company must have a valid A1 digital certificate + * - Webhook must be configured to receive NF-e notifications + * + * **Note:** This resource uses a different API host (api.nfse.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link InboundProductInvoicesResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * // Enable automatic NF-e fetch + * await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + * startFromNsu: '999999', + * environmentSEFAZ: 'Production', + * webhookVersion: '2' + * }); + * + * // Get NF-e details + * const doc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + */ + get inboundProductInvoices(): InboundProductInvoicesResource { + if (!this._inboundProductInvoices) { + this._inboundProductInvoices = new InboundProductInvoicesResource(this.getCteHttpClient()); + } + return this._inboundProductInvoices; + } + + /** + * Product Invoice Query (NF-e) API resource + * + * @description + * Provides read-only operations for querying product invoices (NF-e) directly + * on SEFAZ by access key — no company scope required: + * - Retrieve full invoice details (issuer, buyer, items, totals, transport, payment) + * - Download DANFE PDF + * - Download raw NF-e XML + * - List fiscal events (cancellations, corrections, manifestations) + * + * **Note:** This resource uses a different API host (nfe.api.nfe.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link ProductInvoiceQueryResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * // Retrieve invoice details + * const invoice = await nfe.productInvoiceQuery.retrieve( + * '35240112345678000190550010000001231234567890' + * ); + * console.log(invoice.currentStatus); // 'authorized' + * console.log(invoice.issuer?.name); + * + * // Download PDF + * const pdf = await nfe.productInvoiceQuery.downloadPdf( + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('danfe.pdf', pdf); + * + * // List fiscal events + * const events = await nfe.productInvoiceQuery.listEvents( + * '35240112345678000190550010000001231234567890' + * ); + * ``` + */ + get productInvoiceQuery(): ProductInvoiceQueryResource { + if (!this._productInvoiceQuery) { + this._productInvoiceQuery = new ProductInvoiceQueryResource(this.getNfeQueryHttpClient()); + } + return this._productInvoiceQuery; + } + + /** + * Consumer Invoice Query API resource (CFe-SAT) + * + * @description + * Provides read-only operations for querying CFe-SAT (Cupom Fiscal Eletrônico) + * consumer invoices by access key. No company scope required. + * + * - Retrieve full coupon details (issuer, buyer, items, totals, payment) + * - Download original CFe XML + * + * Uses data API key authentication on `nfe.api.nfe.io`. + * + * @see {@link ConsumerInvoiceQueryResource} + * @throws {ConfigurationError} If API key is not configured + * + * @example + * ```typescript + * // Retrieve coupon details + * const coupon = await nfe.consumerInvoiceQuery.retrieve( + * '35240112345678000190590000000012341234567890' + * ); + * console.log(coupon.currentStatus); // 'Authorized' + * console.log(coupon.issuer?.name); + * + * // Download CFe XML + * const xml = await nfe.consumerInvoiceQuery.downloadXml( + * '35240112345678000190590000000012341234567890' + * ); + * fs.writeFileSync('cfe.xml', xml); + * ``` + */ + get consumerInvoiceQuery(): ConsumerInvoiceQueryResource { + if (!this._consumerInvoiceQuery) { + this._consumerInvoiceQuery = new ConsumerInvoiceQueryResource(this.getNfeQueryHttpClient()); + } + return this._consumerInvoiceQuery; + } + + /** + * Legal Entity Lookup API resource (CNPJ) + * + * @description + * Provides read-only operations for querying Brazilian company (CNPJ) data: + * - Basic company info (Receita Federal registry data) + * - State tax registration (Inscrição Estadual) lookup + * - State tax evaluation for invoice issuance + * - Suggested optimal IE for invoice issuance + * + * **Note:** This resource uses a different API host (legalentity.api.nfe.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link LegalEntityLookupResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * // Basic CNPJ lookup + * const result = await nfe.legalEntityLookup.getBasicInfo('12.345.678/0001-90'); + * console.log(result.legalEntity?.name); + * + * // State tax registration + * const stateTax = await nfe.legalEntityLookup.getStateTaxInfo('SP', '12345678000190'); + * ``` + */ + get legalEntityLookup(): LegalEntityLookupResource { + if (!this._legalEntityLookup) { + this._legalEntityLookup = new LegalEntityLookupResource(this.getLegalEntityHttpClient()); + } + return this._legalEntityLookup; + } + + /** + * Natural Person Lookup API resource (CPF) + * + * @description + * Provides a read-only operation for querying CPF cadastral status (situação cadastral) + * at the Brazilian Federal Revenue Service (Receita Federal). + * + * **Note:** This resource uses a different API host (naturalperson.api.nfe.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link NaturalPersonLookupResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * // CPF cadastral status lookup + * const result = await nfe.naturalPersonLookup.getStatus('123.456.789-01', '1990-01-15'); + * console.log(result.name); // 'JOÃO DA SILVA' + * console.log(result.status); // 'Regular' + * ``` + */ + get naturalPersonLookup(): NaturalPersonLookupResource { + if (!this._naturalPersonLookup) { + this._naturalPersonLookup = new NaturalPersonLookupResource(this.getNaturalPersonHttpClient()); + } + return this._naturalPersonLookup; + } + + /** + * Tax Calculation Engine API resource + * + * @description + * Provides access to the Motor de Cálculo de Tributos (Tax Calculation Engine) + * for computing all applicable Brazilian taxes (ICMS, ICMS-ST, PIS, COFINS, + * IPI, II) on product operations. + * + * **Note:** This resource uses a different API host (api.nfse.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link TaxCalculationResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * const result = await nfe.taxCalculation.calculate('tenant-id', { + * operationType: 'Outgoing', + * issuer: { state: 'SP', taxRegime: 'RealProfit' }, + * recipient: { state: 'RJ' }, + * items: [{ + * id: '1', operationCode: 121, origin: 'National', + * quantity: 10, unitAmount: 100.00, ncm: '61091000' + * }] + * }); + * ``` + */ + get taxCalculation(): TaxCalculationResource { + if (!this._taxCalculation) { + this._taxCalculation = new TaxCalculationResource(this.getCteHttpClient()); + } + return this._taxCalculation; + } + + /** + * Tax Codes API resource (auxiliary reference tables) + * + * @description + * Provides paginated listings of auxiliary tax code reference tables + * needed as inputs for the Tax Calculation Engine: operation codes, + * acquisition purposes, issuer tax profiles, and recipient tax profiles. + * + * **Note:** This resource uses a different API host (api.nfse.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link TaxCodesResource} + * @see {@link TaxCalculationResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * const codes = await nfe.taxCodes.listOperationCodes({ pageIndex: 1, pageCount: 20 }); + * for (const code of codes.items ?? []) { + * console.log(`${code.code} - ${code.description}`); + * } + * ``` + */ + get taxCodes(): TaxCodesResource { + if (!this._taxCodes) { + this._taxCodes = new TaxCodesResource(this.getCteHttpClient()); + } + return this._taxCodes; + } + + /** + * Product Invoices (NF-e) API resource + * + * @description + * Provides full lifecycle management for NF-e (Nota Fiscal Eletrônica de Produto) + * product invoices — issue, list, retrieve, cancel, send correction letters (CC-e), + * disable invoice numbers, and download files (PDF/XML). + * + * **Note:** This resource uses the api.nfse.io host. + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link ProductInvoicesResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * const result = await nfe.productInvoices.create('company-id', invoiceData); + * const invoices = await nfe.productInvoices.list('company-id', { environment: 'Production' }); + * ``` + */ + get productInvoices(): ProductInvoicesResource { + if (!this._productInvoices) { + this._productInvoices = new ProductInvoicesResource(this.getCteHttpClient()); + } + return this._productInvoices; + } + + /** + * State Taxes (Inscrições Estaduais) API resource + * + * @description + * Provides CRUD operations for company state tax registrations required for + * NF-e product invoice issuance — list, create, retrieve, update, and delete. + * + * **Note:** This resource uses the api.nfse.io host. + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link StateTaxesResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * const taxes = await nfe.stateTaxes.list('company-id'); + * const tax = await nfe.stateTaxes.create('company-id', { taxNumber: '123', serie: 1, number: 1 }); + * ``` + */ + get stateTaxes(): StateTaxesResource { + if (!this._stateTaxes) { + this._stateTaxes = new StateTaxesResource(this.getCteHttpClient()); + } + return this._stateTaxes; + } + /** * Create a new NFE.io API client * @@ -328,11 +721,11 @@ export class NfeClient { * }); * ``` * - * @example With only address API key + * @example With only data API key * ```typescript - * // Only use address lookup, no main API access + * // Only use data services (address lookup, CT-e), no main API access * const nfe = new NfeClient({ - * addressApiKey: 'address-api-key' + * dataApiKey: 'data-api-key' * }); * await nfe.addresses.lookupByPostalCode('01310-100'); * ``` @@ -380,10 +773,10 @@ export class NfeClient { */ private getAddressHttpClient(): HttpClient { if (!this._addressHttp) { - const apiKey = this.resolveAddressApiKey(); + const apiKey = this.resolveDataApiKey(); if (!apiKey) { throw new ConfigurationError( - 'API key required for Addresses. Set "addressApiKey" or "apiKey" in config, or NFE_ADDRESS_API_KEY/NFE_API_KEY environment variable.' + 'API key required for data services. Set "dataApiKey" or "apiKey" in config, or NFE_DATA_API_KEY/NFE_API_KEY environment variable.' ); } const httpConfig = buildHttpConfig( @@ -408,18 +801,110 @@ export class NfeClient { } /** - * Resolve the Address API key using fallback chain - * Order: addressApiKey → apiKey → NFE_ADDRESS_API_KEY → NFE_API_KEY + * Resolve the data API key using fallback chain + * Order: dataApiKey → apiKey → NFE_DATA_API_KEY → NFE_API_KEY */ - private resolveAddressApiKey(): string | undefined { + private resolveDataApiKey(): string | undefined { return ( - this.config.addressApiKey || + this.config.dataApiKey || this.config.apiKey || - this.getEnvironmentVariable('NFE_ADDRESS_API_KEY') || + this.getEnvironmentVariable('NFE_DATA_API_KEY') || this.getEnvironmentVariable('NFE_API_KEY') ); } + /** + * Get or create the CT-e API HTTP client + * @throws {ConfigurationError} If no API key is configured + */ + private getCteHttpClient(): HttpClient { + if (!this._cteHttp) { + const apiKey = this.resolveDataApiKey(); + if (!apiKey) { + throw new ConfigurationError( + 'API key required for data services. Set "dataApiKey" or "apiKey" in config, or NFE_DATA_API_KEY/NFE_API_KEY environment variable.' + ); + } + const httpConfig = buildHttpConfig( + apiKey, + CTE_API_BASE_URL, + this.config.timeout, + this.config.retryConfig + ); + this._cteHttp = new HttpClient(httpConfig); + } + return this._cteHttp; + } + + /** + * Get or create the NF-e Query API HTTP client (nfe.api.nfe.io) + * @throws {ConfigurationError} If no API key is configured + */ + private getNfeQueryHttpClient(): HttpClient { + if (!this._nfeQueryHttp) { + const apiKey = this.resolveDataApiKey(); + if (!apiKey) { + throw new ConfigurationError( + 'API key required for data services. Set "dataApiKey" or "apiKey" in config, or NFE_DATA_API_KEY/NFE_API_KEY environment variable.' + ); + } + const httpConfig = buildHttpConfig( + apiKey, + NFE_QUERY_API_BASE_URL, + this.config.timeout, + this.config.retryConfig + ); + this._nfeQueryHttp = new HttpClient(httpConfig); + } + return this._nfeQueryHttp; + } + + /** + * Get or create the Legal Entity API HTTP client (legalentity.api.nfe.io) + * @throws {ConfigurationError} If no API key is configured + */ + private getLegalEntityHttpClient(): HttpClient { + if (!this._legalEntityHttp) { + const apiKey = this.resolveDataApiKey(); + if (!apiKey) { + throw new ConfigurationError( + 'API key required for data services. Set "dataApiKey" or "apiKey" in config, or NFE_DATA_API_KEY/NFE_API_KEY environment variable.' + ); + } + const httpConfig = buildHttpConfig( + apiKey, + LEGAL_ENTITY_API_BASE_URL, + this.config.timeout, + this.config.retryConfig + ); + this._legalEntityHttp = new HttpClient(httpConfig); + } + return this._legalEntityHttp; + } + + /** + * Get or create the Natural Person API HTTP client (naturalperson.api.nfe.io) + * @throws {ConfigurationError} If no API key is configured + */ + private getNaturalPersonHttpClient(): HttpClient { + if (!this._naturalPersonHttp) { + const apiKey = this.resolveDataApiKey(); + if (!apiKey) { + throw new ConfigurationError( + 'API key required for data services. Set "dataApiKey" or "apiKey" in config, or NFE_DATA_API_KEY/NFE_API_KEY environment variable.' + ); + } + const httpConfig = buildHttpConfig( + apiKey, + NATURAL_PERSON_API_BASE_URL, + this.config.timeout, + this.config.retryConfig + ); + this._naturalPersonHttp = new HttpClient(httpConfig); + } + return this._naturalPersonHttp; + } + // -------------------------------------------------------------------------- // Configuration Management // -------------------------------------------------------------------------- @@ -427,7 +912,7 @@ export class NfeClient { private validateAndNormalizeConfig(config: NfeConfig): RequiredNfeConfig { // API keys are now optional - validated lazily when resources are accessed const apiKey = config.apiKey?.trim() || undefined; - const addressApiKey = config.addressApiKey?.trim() || undefined; + const dataApiKey = config.dataApiKey?.trim() || undefined; // Normalize environment const environment = config.environment || 'production'; @@ -446,7 +931,7 @@ export class NfeClient { const normalizedConfig: RequiredNfeConfig = { apiKey, - addressApiKey, + dataApiKey, environment, baseUrl: config.baseUrl || this.getDefaultBaseUrl(), timeout: config.timeout || 30000, @@ -543,8 +1028,8 @@ export class NfeClient { if (normalizedConfig.apiKey === undefined && this.config.apiKey !== undefined && newConfig.apiKey === undefined) { normalizedConfig.apiKey = this.config.apiKey; } - if (normalizedConfig.addressApiKey === undefined && this.config.addressApiKey !== undefined && newConfig.addressApiKey === undefined) { - normalizedConfig.addressApiKey = this.config.addressApiKey; + if (normalizedConfig.dataApiKey === undefined && this.config.dataApiKey !== undefined && newConfig.dataApiKey === undefined) { + normalizedConfig.dataApiKey = this.config.dataApiKey; } // Update internal config @@ -553,12 +1038,14 @@ export class NfeClient { // Clear cached HTTP clients and resources so they're recreated with new config this._http = undefined; this._addressHttp = undefined; + this._cteHttp = undefined; this._serviceInvoices = undefined; this._companies = undefined; this._legalPeople = undefined; this._naturalPeople = undefined; this._webhooks = undefined; this._addresses = undefined; + this._transportationInvoices = undefined; } /** diff --git a/src/core/http/client.ts b/src/core/http/client.ts index ba24aad..65a091c 100644 --- a/src/core/http/client.ts +++ b/src/core/http/client.ts @@ -64,6 +64,16 @@ export class HttpClient { return this.request('DELETE', url); } + /** + * GET request expecting a binary buffer response (e.g., PDF, XML downloads). + * + * Sends an Accept header for the given content type and returns the response body as a Buffer. + */ + async getBuffer(path: string, accept: string = 'application/octet-stream'): Promise> { + const url = this.buildUrl(path); + return this.request('GET', url, undefined, { 'Accept': accept }); + } + // -------------------------------------------------------------------------- // Core Request Method with Retry Logic // -------------------------------------------------------------------------- diff --git a/src/core/resources/addresses.ts b/src/core/resources/addresses.ts index 4b648d0..39d2c19 100644 --- a/src/core/resources/addresses.ts +++ b/src/core/resources/addresses.ts @@ -68,7 +68,7 @@ function normalizePostalCode(postalCode: string): string { * Data is sourced from Correios DNE (Diretório Nacional de Endereços) integrated with IBGE city codes. * * **Note:** This resource uses a different API host (address.api.nfe.io) and may require - * a separate API key configured via `addressApiKey` in the client configuration. + * a separate API key configured via `dataApiKey` in the client configuration. * * @example Basic postal code lookup * ```typescript diff --git a/src/core/resources/consumer-invoice-query.ts b/src/core/resources/consumer-invoice-query.ts new file mode 100644 index 0000000..87d0be7 --- /dev/null +++ b/src/core/resources/consumer-invoice-query.ts @@ -0,0 +1,141 @@ +/** + * NFE.io SDK v3 - Consumer Invoice Query Resource + * + * Queries CFe-SAT (Cupom Fiscal Eletrônico) consumer invoices + * by access key. Read-only lookups — no company scope required. + * Uses the API host: nfe.api.nfe.io + */ + +import type { HttpClient } from '../http/client.js'; +import type { TaxCoupon } from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Regex pattern for valid access key (44 numeric digits) */ +const ACCESS_KEY_PATTERN = /^\d{44}$/; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates access key format (44 numeric digits) + * @param accessKey - The access key to validate + * @throws {ValidationError} If access key is empty or has invalid format + */ +function validateAccessKey(accessKey: string): void { + if (!accessKey || accessKey.trim() === '') { + throw new ValidationError('Access key is required'); + } + + const normalized = accessKey.trim(); + if (!ACCESS_KEY_PATTERN.test(normalized)) { + throw new ValidationError( + `Invalid access key: "${accessKey}". Expected 44 numeric digits.` + ); + } +} + +// ============================================================================ +// Resource Implementation +// ============================================================================ + +/** + * Consumer Invoice Query Resource + * + * @description + * Queries CFe-SAT (Cupom Fiscal Eletrônico) consumer invoices by access key. + * This is a read-only resource that does not require company scope. + * + * **Capabilities:** + * - Retrieve full coupon details (issuer, buyer, items, totals, payment) + * - Download original CFe XML + * + * **Authentication:** Uses data API key (`dataApiKey` or `apiKey` fallback). + * + * @example + * ```typescript + * const coupon = await nfe.consumerInvoiceQuery.retrieve( + * '35240112345678000190590000000012341234567890' + * ); + * console.log(coupon.issuer?.name, coupon.totals?.couponAmount); + * ``` + */ +export class ConsumerInvoiceQueryResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Public Methods + // -------------------------------------------------------------------------- + + /** + * Retrieve full CFe-SAT coupon details from SEFAZ by access key + * + * @param accessKey - 44-digit numeric access key (Chave de Acesso) + * @returns Full coupon details including issuer, buyer, items, totals, and payment + * @throws {ValidationError} If access key format is invalid + * @throws {NotFoundError} If no coupon matches the access key (HTTP 404) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const coupon = await nfe.consumerInvoiceQuery.retrieve( + * '35240112345678000190590000000012341234567890' + * ); + * console.log(coupon.currentStatus); // 'Authorized' + * console.log(coupon.issuer?.name); + * console.log(coupon.totals?.couponAmount); + * ``` + */ + async retrieve(accessKey: string): Promise { + validateAccessKey(accessKey); + const response = await this.http.get( + `/v1/consumerinvoices/coupon/${accessKey.trim()}` + ); + return response.data; + } + + /** + * Download the raw CFe XML for a consumer invoice by access key + * + * @param accessKey - 44-digit numeric access key (Chave de Acesso) + * @returns Buffer containing the XML binary content + * @throws {ValidationError} If access key format is invalid + * @throws {NotFoundError} If no coupon matches the access key (HTTP 404) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const xmlBuffer = await nfe.consumerInvoiceQuery.downloadXml( + * '35240112345678000190590000000012341234567890' + * ); + * fs.writeFileSync('cfe.xml', xmlBuffer); + * ``` + */ + async downloadXml(accessKey: string): Promise { + validateAccessKey(accessKey); + const response = await this.http.getBuffer( + `/v1/consumerinvoices/coupon/${accessKey.trim()}.xml`, + 'application/xml' + ); + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a new ConsumerInvoiceQueryResource instance + */ +export function createConsumerInvoiceQueryResource(http: HttpClient): ConsumerInvoiceQueryResource { + return new ConsumerInvoiceQueryResource(http); +} diff --git a/src/core/resources/inbound-product-invoices.ts b/src/core/resources/inbound-product-invoices.ts new file mode 100644 index 0000000..8849442 --- /dev/null +++ b/src/core/resources/inbound-product-invoices.ts @@ -0,0 +1,654 @@ +/** + * NFE.io SDK v3 - Inbound Product Invoices Resource + * + * Handles NF-e (Nota Fiscal Eletrônica) distribution queries via Distribuição DFe. + * Uses the API host: api.nfse.io + */ + +import type { HttpClient } from '../http/client.js'; +import type { + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings, + EnableInboundOptions, + ManifestEventType +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Regex pattern for valid access key (44 numeric digits) */ +const ACCESS_KEY_PATTERN = /^\d{44}$/; + +/** Default manifest event type: Ciência da Operação */ +const DEFAULT_MANIFEST_EVENT_TYPE: ManifestEventType = 210210; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates company ID is not empty + * @param companyId - The company ID to validate + * @throws {ValidationError} If company ID is empty + */ +function validateCompanyId(companyId: string): void { + if (!companyId || companyId.trim() === '') { + throw new ValidationError('Company ID is required'); + } +} + +/** + * Validates access key format (44 numeric digits) + * @param accessKey - The access key to validate + * @throws {ValidationError} If access key format is invalid + */ +function validateAccessKey(accessKey: string): void { + if (!accessKey || accessKey.trim() === '') { + throw new ValidationError('Access key is required'); + } + + const normalized = accessKey.trim(); + if (!ACCESS_KEY_PATTERN.test(normalized)) { + throw new ValidationError( + `Invalid access key: "${accessKey}". Expected 44 numeric digits.` + ); + } +} + +/** + * Validates event key is not empty + * @param eventKey - The event key to validate + * @throws {ValidationError} If event key is empty + */ +function validateEventKey(eventKey: string): void { + if (!eventKey || eventKey.trim() === '') { + throw new ValidationError('Event key is required'); + } +} + +/** + * Validates access key or NSU identifier is not empty + * @param accessKeyOrNsu - The identifier to validate + * @throws {ValidationError} If identifier is empty + */ +function validateAccessKeyOrNsu(accessKeyOrNsu: string): void { + if (!accessKeyOrNsu || accessKeyOrNsu.trim() === '') { + throw new ValidationError('Access key or NSU is required'); + } +} + +// ============================================================================ +// Inbound Product Invoices Resource +// ============================================================================ + +/** + * Inbound Product Invoices (NF-e Distribution) API Resource + * + * @description + * Provides operations for querying NF-e (Nota Fiscal Eletrônica) documents + * received by a company via the SEFAZ Distribuição DFe service. + * + * **Capabilities:** + * - Enable/disable automatic NF-e distribution fetch + * - Retrieve inbound NF-e metadata by access key + * - Download NF-e documents in XML, PDF, and JSON formats + * - Send recipient manifest (Manifestação do Destinatário) + * - Reprocess webhooks + * + * **Prerequisites:** + * - Company must be registered with a valid A1 digital certificate + * - Webhook must be configured to receive NF-e notifications + * + * **Note:** This resource uses a different API host (api.nfse.io) and may require + * a separate API key configured via `dataApiKey` in the client configuration. + * If not set, it falls back to `apiKey`. + * + * @example Enable automatic NF-e search + * ```typescript + * const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + * startFromNsu: '999999', + * environmentSEFAZ: 'Production', + * webhookVersion: '2' + * }); + * ``` + * + * @example Retrieve NF-e details + * ```typescript + * const details = await nfe.inboundProductInvoices.getProductInvoiceDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log(details.nameSender, details.totalInvoiceAmount); + * ``` + * + * @example Download NF-e XML + * ```typescript + * const xml = await nfe.inboundProductInvoices.getXml( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + */ +export class InboundProductInvoicesResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Automatic Search Management + // -------------------------------------------------------------------------- + + /** + * Enable automatic NF-e distribution fetch for a company + * + * Activates the automatic search for NF-e documents destined to the specified + * company via SEFAZ Distribuição DFe. Once enabled, new NF-e documents will be + * automatically retrieved and sent to the configured webhook endpoint. + * + * @param companyId - The company ID to enable automatic search for + * @param options - Configuration options for the automatic search + * @returns Promise with the inbound settings after enabling + * @throws {ValidationError} If company ID is empty + * @throws {BadRequestError} If the request is invalid + * @throws {NotFoundError} If the company is not found + * + * @example + * ```typescript + * const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + * startFromNsu: '999999', + * startFromDate: '2024-01-01T00:00:00Z', + * environmentSEFAZ: 'Production', + * automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + * webhookVersion: '2' + * }); + * console.log('Status:', settings.status); + * ``` + */ + async enableAutoFetch( + companyId: string, + options: EnableInboundOptions + ): Promise { + validateCompanyId(companyId); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/productinvoices`, + options + ); + + return response.data; + } + + /** + * Disable automatic NF-e distribution fetch for a company + * + * Deactivates the automatic search for NF-e documents. After disabling, + * no new NF-e documents will be retrieved for the company. + * + * @param companyId - The company ID to disable automatic search for + * @returns Promise with the inbound settings after disabling + * @throws {ValidationError} If company ID is empty + * @throws {NotFoundError} If automatic search is not enabled for this company + * + * @example + * ```typescript + * const settings = await nfe.inboundProductInvoices.disableAutoFetch('company-id'); + * console.log('Disabled. Status:', settings.status); + * ``` + */ + async disableAutoFetch(companyId: string): Promise { + validateCompanyId(companyId); + + const response = await this.http.delete( + `/v2/companies/${companyId}/inbound/productinvoices` + ); + + return response.data; + } + + /** + * Get current automatic NF-e distribution fetch settings + * + * Retrieves the current configuration for automatic NF-e search, + * including status, start NSU, start date, and timestamps. + * + * @param companyId - The company ID to get settings for + * @returns Promise with the current inbound settings + * @throws {ValidationError} If company ID is empty + * @throws {NotFoundError} If automatic search is not configured for this company + * + * @example + * ```typescript + * const settings = await nfe.inboundProductInvoices.getSettings('company-id'); + * console.log('Status:', settings.status); + * console.log('Start NSU:', settings.startFromNsu); + * console.log('Webhook version:', settings.webhookVersion); + * ``` + */ + async getSettings(companyId: string): Promise { + validateCompanyId(companyId); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoices` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Document Detail Operations + // -------------------------------------------------------------------------- + + /** + * Get details of an inbound NF-e/CT-e by access key (webhook v1 format) + * + * Retrieves the metadata of an inbound document using its 44-digit access key. + * This is the generic endpoint that works for both NF-e and CT-e documents. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the inbound invoice metadata + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const doc = await nfe.inboundProductInvoices.getDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log('Sender:', doc.nameSender); + * console.log('Amount:', doc.totalInvoiceAmount); + * console.log('NSU:', doc.nsu); + * ``` + */ + async getDetails( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}` + ); + + return response.data; + } + + /** + * Get details of an inbound NF-e by access key (webhook v2 format) + * + * Retrieves the metadata of an NF-e document using its 44-digit access key. + * This endpoint returns additional `productInvoices` array compared to the v1 format. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the inbound product invoice metadata (includes productInvoices array) + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const doc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log('Sender:', doc.nameSender); + * console.log('Product invoices:', doc.productInvoices.length); + * ``` + */ + async getProductInvoiceDetails( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKey.trim()}` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Event Detail Operations + // -------------------------------------------------------------------------- + + /** + * Get details of an event related to an inbound NF-e/CT-e (generic endpoint) + * + * Retrieves the metadata of an event associated with an inbound document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key of the parent document + * @param eventKey - The event key + * @returns Promise with the event metadata + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const event = await nfe.inboundProductInvoices.getEventDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 'event-key-123' + * ); + * console.log('Event:', event.description); + * ``` + */ + async getEventDetails( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + validateEventKey(eventKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/events/${eventKey.trim()}` + ); + + return response.data; + } + + /** + * Get details of an event related to an inbound NF-e (product invoice endpoint) + * + * Retrieves the metadata of an event associated with an inbound NF-e document. + * Returns the webhook v2 format with `productInvoices` array. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key of the parent document + * @param eventKey - The event key + * @returns Promise with the product invoice event metadata + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const event = await nfe.inboundProductInvoices.getProductInvoiceEventDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 'event-key-123' + * ); + * console.log('Event:', event.description); + * console.log('Product invoices:', event.productInvoices.length); + * ``` + */ + async getProductInvoiceEventDetails( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + validateEventKey(eventKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKey.trim()}/events/${eventKey.trim()}` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // File Download Operations + // -------------------------------------------------------------------------- + + /** + * Download XML of an inbound NF-e/CT-e by access key + * + * Gets the XML content of an inbound document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the XML content as a string + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const xml = await nfe.inboundProductInvoices.getXml( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('nfe.xml', xml); + * ``` + */ + async getXml(companyId: string, accessKey: string): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/xml` + ); + + return response.data; + } + + /** + * Download XML of an event related to an inbound NF-e/CT-e + * + * Gets the XML content of an event associated with an inbound document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key of the parent document + * @param eventKey - The event key + * @returns Promise with the event XML content as a string + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const xml = await nfe.inboundProductInvoices.getEventXml( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 'event-key-123' + * ); + * fs.writeFileSync('nfe-event.xml', xml); + * ``` + */ + async getEventXml( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + validateEventKey(eventKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/events/${eventKey.trim()}/xml` + ); + + return response.data; + } + + /** + * Download PDF of an inbound NF-e by access key + * + * Gets the PDF content of an NF-e document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the PDF content as a string + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const pdf = await nfe.inboundProductInvoices.getPdf( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('nfe.pdf', pdf); + * ``` + */ + async getPdf(companyId: string, accessKey: string): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/pdf` + ); + + return response.data; + } + + /** + * Get JSON representation of an inbound NF-e by access key + * + * Gets the structured JSON data of an NF-e document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the NF-e metadata in JSON format + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const data = await nfe.inboundProductInvoices.getJson( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log('Sender:', data.nameSender); + * console.log('Amount:', data.totalInvoiceAmount); + * ``` + */ + async getJson( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKey.trim()}/json` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Manifest Operations + // -------------------------------------------------------------------------- + + /** + * Send recipient manifest (Manifestação do Destinatário) for an NF-e + * + * Sends a manifest event for an NF-e document identified by its access key. + * Defaults to "Ciência da Operação" (210210) if no event type is specified. + * + * **Event types:** + * - `210210` — Ciência da Operação (awareness, default) + * - `210220` — Confirmação da Operação (confirmation) + * - `210240` — Operação não Realizada (operation not performed) + * + * @param companyId - The company ID + * @param accessKey - The 44-digit access key of the NF-e + * @param tpEvent - Manifest event type (defaults to 210210) + * @returns Promise with the manifest response + * @throws {ValidationError} If company ID or access key is invalid + * + * @example Default manifest (Ciência da Operação) + * ```typescript + * const result = await nfe.inboundProductInvoices.manifest( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + * + * @example Confirm operation + * ```typescript + * const result = await nfe.inboundProductInvoices.manifest( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 210220 + * ); + * ``` + */ + async manifest( + companyId: string, + accessKey: string, + tpEvent: ManifestEventType = DEFAULT_MANIFEST_EVENT_TYPE + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/manifest?tpEvent=${tpEvent}` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Webhook Operations + // -------------------------------------------------------------------------- + + /** + * Reprocess webhook for an inbound NF-e by access key or NSU + * + * Triggers reprocessing of the webhook notification for a specific document, + * identified either by its 44-digit access key or by its NSU number. + * + * @param companyId - The company ID + * @param accessKeyOrNsu - The 44-digit access key or NSU number + * @returns Promise with the product invoice metadata + * @throws {ValidationError} If company ID or identifier is empty + * @throws {NotFoundError} If the document is not found + * + * @example Reprocess by access key + * ```typescript + * const result = await nfe.inboundProductInvoices.reprocessWebhook( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + * + * @example Reprocess by NSU + * ```typescript + * const result = await nfe.inboundProductInvoices.reprocessWebhook( + * 'company-id', + * '12345' + * ); + * ``` + */ + async reprocessWebhook( + companyId: string, + accessKeyOrNsu: string + ): Promise { + validateCompanyId(companyId); + validateAccessKeyOrNsu(accessKeyOrNsu); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKeyOrNsu.trim()}/processwebhook` + ); + + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Creates an InboundProductInvoicesResource instance + * + * @param http - HTTP client configured for the inbound API (api.nfse.io) + * @returns InboundProductInvoicesResource instance + */ +export function createInboundProductInvoicesResource( + http: HttpClient +): InboundProductInvoicesResource { + return new InboundProductInvoicesResource(http); +} diff --git a/src/core/resources/index.ts b/src/core/resources/index.ts index 4317456..794d90c 100644 --- a/src/core/resources/index.ts +++ b/src/core/resources/index.ts @@ -11,3 +11,13 @@ export { LegalPeopleResource } from './legal-people.js'; export { NaturalPeopleResource } from './natural-people.js'; export { WebhooksResource } from './webhooks.js'; export { AddressesResource, createAddressesResource, ADDRESS_API_BASE_URL } from './addresses.js'; +export { TransportationInvoicesResource, createTransportationInvoicesResource, CTE_API_BASE_URL } from './transportation-invoices.js'; +export { InboundProductInvoicesResource, createInboundProductInvoicesResource } from './inbound-product-invoices.js'; +export { ProductInvoiceQueryResource, createProductInvoiceQueryResource, NFE_QUERY_API_BASE_URL } from './product-invoice-query.js'; +export { ConsumerInvoiceQueryResource, createConsumerInvoiceQueryResource } from './consumer-invoice-query.js'; +export { LegalEntityLookupResource, createLegalEntityLookupResource, LEGAL_ENTITY_API_BASE_URL } from './legal-entity-lookup.js'; +export { NaturalPersonLookupResource, createNaturalPersonLookupResource, NATURAL_PERSON_API_BASE_URL } from './natural-person-lookup.js'; +export { TaxCalculationResource, createTaxCalculationResource } from './tax-calculation.js'; +export { TaxCodesResource, createTaxCodesResource } from './tax-codes.js'; +export { ProductInvoicesResource } from './product-invoices.js'; +export { StateTaxesResource } from './state-taxes.js'; diff --git a/src/core/resources/legal-entity-lookup.ts b/src/core/resources/legal-entity-lookup.ts new file mode 100644 index 0000000..15f4dec --- /dev/null +++ b/src/core/resources/legal-entity-lookup.ts @@ -0,0 +1,334 @@ +/** + * NFE.io SDK v3 - Legal Entity Lookup Resource + * + * Handles CNPJ lookup operations via the Legal Entity API. + * Uses a separate API host: legalentity.api.nfe.io + * + * Provides methods for: + * - Basic company info lookup by CNPJ + * - State tax registration (Inscrição Estadual) lookup + * - State tax evaluation for invoice issuance + * - Suggested state tax for optimal invoice issuance + */ + +import type { HttpClient } from '../http/client.js'; +import type { + BrazilianState, + LegalEntityBasicInfoOptions, + LegalEntityBasicInfoResponse, + LegalEntityStateTaxResponse, + LegalEntityStateTaxForInvoiceResponse, +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Base URL for Legal Entity API */ +export const LEGAL_ENTITY_API_BASE_URL = 'https://legalentity.api.nfe.io'; + +/** Set of valid Brazilian state codes (27 UFs + EX + NA) */ +const VALID_BRAZILIAN_STATES: ReadonlySet = new Set([ + 'AC', 'AL', 'AM', 'AP', 'BA', 'CE', 'DF', 'ES', 'GO', + 'MA', 'MG', 'MS', 'MT', 'PA', 'PB', 'PE', 'PI', 'PR', + 'RJ', 'RN', 'RO', 'RR', 'RS', 'SC', 'SE', 'SP', 'TO', + 'EX', 'NA', +]); + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Normalizes a federal tax number (CNPJ) by stripping all non-digit characters. + * + * @param federalTaxNumber - Raw CNPJ string, with or without punctuation + * @returns Digits-only CNPJ string + */ +function normalizeFederalTaxNumber(federalTaxNumber: string): string { + return federalTaxNumber.replace(/\D/g, ''); +} + +/** + * Validates a federal tax number (CNPJ) format. + * Strips non-digit characters and checks for exactly 14 digits. + * + * @param federalTaxNumber - CNPJ string to validate + * @returns Normalized digits-only CNPJ string + * @throws {ValidationError} If input is empty or not exactly 14 digits after normalization + */ +function validateFederalTaxNumber(federalTaxNumber: string | undefined | null): string { + if (!federalTaxNumber || federalTaxNumber.trim() === '') { + throw new ValidationError('Federal tax number (CNPJ) is required'); + } + + const normalized = normalizeFederalTaxNumber(federalTaxNumber); + + if (normalized.length !== 14) { + throw new ValidationError( + `Invalid federal tax number format: "${federalTaxNumber}". Expected 14 digits (e.g., "12345678000190" or "12.345.678/0001-90"), got ${normalized.length} digit(s).` + ); + } + + return normalized; +} + +/** + * Validates a Brazilian state code. + * Normalizes to uppercase and checks against the valid set. + * + * @param state - State code to validate + * @returns Normalized uppercase state code + * @throws {ValidationError} If state code is empty or not in the valid set + */ +function validateState(state: string | undefined | null): BrazilianState { + if (!state || state.trim() === '') { + throw new ValidationError('State code is required'); + } + + const normalized = state.trim().toUpperCase(); + + if (!VALID_BRAZILIAN_STATES.has(normalized)) { + const validCodes = Array.from(VALID_BRAZILIAN_STATES).sort().join(', '); + throw new ValidationError( + `Invalid state code: "${state}". Valid codes: ${validCodes}` + ); + } + + return normalized as BrazilianState; +} + +// ============================================================================ +// Legal Entity Lookup Resource +// ============================================================================ + +/** + * Legal Entity Lookup API Resource + * + * @description + * Provides read-only operations for querying Brazilian company (CNPJ) data + * from the NFE.io Legal Entity API. Data is sourced from Receita Federal, + * SEFAZ state registries, and NFE.io enrichment services. + * + * **Note:** This resource uses a different API host (legalentity.api.nfe.io) + * and may require a separate API key configured via `dataApiKey` in the client configuration. + * + * @example Basic CNPJ lookup + * ```typescript + * const result = await nfe.legalEntityLookup.getBasicInfo('12.345.678/0001-90'); + * console.log(result.legalEntity?.name); // 'EMPRESA LTDA' + * console.log(result.legalEntity?.status); // 'Active' + * console.log(result.legalEntity?.address?.city?.name); // 'São Paulo' + * ``` + * + * @example State tax registration lookup + * ```typescript + * const result = await nfe.legalEntityLookup.getStateTaxInfo('SP', '12345678000190'); + * for (const tax of result.legalEntity?.stateTaxes ?? []) { + * console.log(`IE: ${tax.taxNumber} - Status: ${tax.status}`); + * } + * ``` + * + * @example Best IE for invoice issuance + * ```typescript + * const result = await nfe.legalEntityLookup.getSuggestedStateTaxForInvoice('SP', '12345678000190'); + * const bestIE = result.legalEntity?.stateTaxes?.[0]; + * console.log(`Best IE: ${bestIE?.taxNumber} (${bestIE?.status})`); + * ``` + */ +export class LegalEntityLookupResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Public Methods + // -------------------------------------------------------------------------- + + /** + * Lookup basic company information by CNPJ + * + * @description + * Queries the Receita Federal registry for company registration data including + * legal name, trade name, address, phone numbers, economic activities (CNAE), + * legal nature, partners, registration status, and share capital. + * + * @param federalTaxNumber - CNPJ number, with or without punctuation (e.g., "12345678000190" or "12.345.678/0001-90") + * @param options - Optional lookup configuration + * @returns Promise with company basic information + * @throws {ValidationError} If CNPJ format is invalid (not 14 digits) + * @throws {NotFoundError} If no company found for the given CNPJ + * @throws {AuthenticationError} If API key is invalid or missing + * + * @example + * ```typescript + * // Simple lookup + * const result = await nfe.legalEntityLookup.getBasicInfo('12345678000190'); + * console.log(result.legalEntity?.name); + * + * // With formatted CNPJ + * const result = await nfe.legalEntityLookup.getBasicInfo('12.345.678/0001-90'); + * + * // Disable address update from postal service + * const result = await nfe.legalEntityLookup.getBasicInfo('12345678000190', { + * updateAddress: false, + * updateCityCode: true + * }); + * ``` + */ + async getBasicInfo( + federalTaxNumber: string, + options?: LegalEntityBasicInfoOptions + ): Promise { + const normalized = validateFederalTaxNumber(federalTaxNumber); + + const params: Record = {}; + if (options?.updateAddress !== undefined) { + params['updateAddress'] = options.updateAddress; + } + if (options?.updateCityCode !== undefined) { + params['updateCityCode'] = options.updateCityCode; + } + + const response = await this.http.get( + `/v2/legalentities/basicInfo/${normalized}`, + Object.keys(params).length > 0 ? params : undefined + ); + + return response.data; + } + + /** + * Lookup state tax registration (Inscrição Estadual) by CNPJ and state + * + * @description + * Queries state tax registration data for a given CNPJ in a specific Brazilian state. + * Returns registration details including status, tax regime, economic activities, + * and fiscal document indicators (NFe, NFSe, CTe, NFCe). + * + * @param state - Brazilian state abbreviation (e.g., "SP", "RJ", "MG") + * @param federalTaxNumber - CNPJ number, with or without punctuation + * @returns Promise with state tax registration information + * @throws {ValidationError} If state code or CNPJ format is invalid + * @throws {AuthenticationError} If API key is invalid or missing + * + * @example + * ```typescript + * const result = await nfe.legalEntityLookup.getStateTaxInfo('SP', '12345678000190'); + * console.log(result.legalEntity?.taxRegime); // 'SimplesNacional' + * + * for (const tax of result.legalEntity?.stateTaxes ?? []) { + * console.log(`IE: ${tax.taxNumber} - Status: ${tax.status}`); + * console.log(` NFe: ${tax.nfe?.status}, NFSe: ${tax.nfse?.status}`); + * } + * ``` + */ + async getStateTaxInfo( + state: string, + federalTaxNumber: string + ): Promise { + const normalizedState = validateState(state); + const normalizedCnpj = validateFederalTaxNumber(federalTaxNumber); + + const response = await this.http.get( + `/v2/legalentities/stateTaxInfo/${normalizedState}/${normalizedCnpj}` + ); + + return response.data; + } + + /** + * Lookup state tax registration for invoice issuance evaluation + * + * @description + * Queries state tax registration data specifically for evaluating the ability + * to issue product invoices (NF-e) in a given state. Returns extended status + * information including temporary and unconfirmed states. + * + * @param state - Brazilian state abbreviation (e.g., "SP", "RJ", "MG") + * @param federalTaxNumber - CNPJ number, with or without punctuation + * @returns Promise with state tax data for invoice evaluation + * @throws {ValidationError} If state code or CNPJ format is invalid + * @throws {AuthenticationError} If API key is invalid or missing + * + * @example + * ```typescript + * const result = await nfe.legalEntityLookup.getStateTaxForInvoice('MG', '12345678000190'); + * for (const tax of result.legalEntity?.stateTaxes ?? []) { + * if (tax.status === 'Abled') { + * console.log(`Can issue invoices with IE: ${tax.taxNumber}`); + * } + * } + * ``` + */ + async getStateTaxForInvoice( + state: string, + federalTaxNumber: string + ): Promise { + const normalizedState = validateState(state); + const normalizedCnpj = validateFederalTaxNumber(federalTaxNumber); + + const response = await this.http.get( + `/v2/legalentities/stateTaxForInvoice/${normalizedState}/${normalizedCnpj}` + ); + + return response.data; + } + + /** + * Lookup the best state tax registration for invoice issuance + * + * @description + * Queries the optimal state tax registration for issuing invoices when multiple + * registrations are enabled in a state. NFE.io applies evaluation criteria to + * suggest the best IE for invoice issuance. + * + * Returns the same response type as `getStateTaxForInvoice` but the API + * prioritizes the best enabled state tax registration. + * + * @param state - Brazilian state abbreviation (e.g., "SP", "RJ", "MG") + * @param federalTaxNumber - CNPJ number, with or without punctuation + * @returns Promise with suggested state tax data for invoice evaluation + * @throws {ValidationError} If state code or CNPJ format is invalid + * @throws {AuthenticationError} If API key is invalid or missing + * + * @example + * ```typescript + * const result = await nfe.legalEntityLookup.getSuggestedStateTaxForInvoice('SP', '12345678000190'); + * const bestIE = result.legalEntity?.stateTaxes?.[0]; + * if (bestIE) { + * console.log(`Recommended IE: ${bestIE.taxNumber} (${bestIE.status})`); + * } + * ``` + */ + async getSuggestedStateTaxForInvoice( + state: string, + federalTaxNumber: string + ): Promise { + const normalizedState = validateState(state); + const normalizedCnpj = validateFederalTaxNumber(federalTaxNumber); + + const response = await this.http.get( + `/v2/legalentities/stateTaxSuggestedForInvoice/${normalizedState}/${normalizedCnpj}` + ); + + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Creates a LegalEntityLookupResource instance + * + * @param http - HTTP client configured for the Legal Entity API + * @returns LegalEntityLookupResource instance + */ +export function createLegalEntityLookupResource(http: HttpClient): LegalEntityLookupResource { + return new LegalEntityLookupResource(http); +} diff --git a/src/core/resources/natural-person-lookup.ts b/src/core/resources/natural-person-lookup.ts new file mode 100644 index 0000000..fd5b9af --- /dev/null +++ b/src/core/resources/natural-person-lookup.ts @@ -0,0 +1,214 @@ +/** + * NFE.io SDK v3 - Natural Person Lookup Resource + * + * Handles CPF cadastral status lookup operations via the Natural Person API. + * Uses a separate API host: naturalperson.api.nfe.io + * + * Provides methods for: + * - CPF cadastral status query (situação cadastral na Receita Federal) + */ + +import type { HttpClient } from '../http/client.js'; +import type { NaturalPersonStatusResponse } from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Base URL for Natural Person API */ +export const NATURAL_PERSON_API_BASE_URL = 'https://naturalperson.api.nfe.io'; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Normalizes a CPF by stripping all non-digit characters. + * + * @param cpf - Raw CPF string, with or without punctuation + * @returns Digits-only CPF string + */ +function normalizeCpf(cpf: string): string { + return cpf.replace(/\D/g, ''); +} + +/** + * Validates a CPF format. + * Strips non-digit characters and checks for exactly 11 digits. + * + * @param federalTaxNumber - CPF string to validate + * @returns Normalized digits-only CPF string + * @throws {ValidationError} If input is empty or not exactly 11 digits after normalization + */ +function validateCpf(federalTaxNumber: string | undefined | null): string { + if (!federalTaxNumber || federalTaxNumber.trim() === '') { + throw new ValidationError('Federal tax number (CPF) is required'); + } + + const normalized = normalizeCpf(federalTaxNumber); + + if (normalized.length !== 11) { + throw new ValidationError( + `Invalid federal tax number format: "${federalTaxNumber}". Expected 11 digits (e.g., "12345678901" or "123.456.789-01"), got ${normalized.length} digit(s).` + ); + } + + return normalized; +} + +/** + * Validates and normalizes a birth date parameter. + * Accepts a string in YYYY-MM-DD format or a Date object. + * + * @param birthDate - Birth date as string (YYYY-MM-DD) or Date object + * @returns Normalized YYYY-MM-DD string + * @throws {ValidationError} If input is empty, invalid format, or invalid date values + */ +function validateBirthDate(birthDate: string | Date | undefined | null): string { + if (birthDate === undefined || birthDate === null) { + throw new ValidationError('Birth date is required'); + } + + // Convert Date object to YYYY-MM-DD string using UTC + if (birthDate instanceof Date) { + if (isNaN(birthDate.getTime())) { + throw new ValidationError('Birth date is an invalid Date object'); + } + const year = birthDate.getUTCFullYear(); + const month = String(birthDate.getUTCMonth() + 1).padStart(2, '0'); + const day = String(birthDate.getUTCDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; + } + + // Validate string format + if (typeof birthDate === 'string') { + if (birthDate.trim() === '') { + throw new ValidationError('Birth date is required'); + } + + const match = birthDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); + if (!match) { + throw new ValidationError( + `Invalid birth date format: "${birthDate}". Expected YYYY-MM-DD format (e.g., "1990-01-15").` + ); + } + + const monthStr = match[2] as string; + const dayStr = match[3] as string; + const month = parseInt(monthStr, 10); + const day = parseInt(dayStr, 10); + + if (month < 1 || month > 12) { + throw new ValidationError( + `Invalid birth date: "${birthDate}". Month must be between 01 and 12, got ${monthStr}.` + ); + } + + if (day < 1 || day > 31) { + throw new ValidationError( + `Invalid birth date: "${birthDate}". Day must be between 01 and 31, got ${dayStr}.` + ); + } + + return birthDate; + } + + throw new ValidationError('Birth date must be a string (YYYY-MM-DD) or a Date object'); +} + +// ============================================================================ +// Natural Person Lookup Resource +// ============================================================================ + +/** + * Natural Person Lookup API Resource + * + * @description + * Provides a read-only operation for querying CPF cadastral status (situação cadastral) + * at the Brazilian Federal Revenue Service (Receita Federal) via the NFE.io Natural Person API. + * + * **Note:** This resource uses a different API host (naturalperson.api.nfe.io) + * and may require a separate API key configured via `dataApiKey` in the client configuration. + * + * @example CPF cadastral status lookup + * ```typescript + * const result = await nfe.naturalPersonLookup.getStatus('123.456.789-01', '1990-01-15'); + * console.log(result.name); // 'JOÃO DA SILVA' + * console.log(result.status); // 'Regular' + * ``` + * + * @example Using a Date object for birth date + * ```typescript + * const result = await nfe.naturalPersonLookup.getStatus('12345678901', new Date(1990, 0, 15)); + * console.log(result.status); // 'Regular' + * ``` + */ +export class NaturalPersonLookupResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Public Methods + // -------------------------------------------------------------------------- + + /** + * Lookup CPF cadastral status at Receita Federal + * + * @description + * Queries the cadastral status of a CPF (pessoa física) at the Brazilian Federal + * Revenue Service. Returns the person's name, CPF, birth date, cadastral status + * (Regular, Suspensa, Cancelada, etc.), and query timestamp. + * + * @param federalTaxNumber - CPF number, with or without punctuation (e.g., "12345678901" or "123.456.789-01") + * @param birthDate - Date of birth as string in YYYY-MM-DD format (e.g., "1990-01-15") or a Date object + * @returns Promise with the CPF cadastral status response + * @throws {ValidationError} If CPF format is invalid (not 11 digits) or birth date format is invalid + * @throws {NotFoundError} If CPF is not found or birth date does not match (404) + * @throws {AuthenticationError} If API key is invalid or missing (401) + * + * @example + * ```typescript + * // Simple lookup with string date + * const result = await nfe.naturalPersonLookup.getStatus('12345678901', '1990-01-15'); + * console.log(result.name); // 'JOÃO DA SILVA' + * console.log(result.status); // 'Regular' + * + * // With formatted CPF + * const result = await nfe.naturalPersonLookup.getStatus('123.456.789-01', '1990-01-15'); + * + * // Using a Date object + * const result = await nfe.naturalPersonLookup.getStatus('12345678901', new Date(1990, 0, 15)); + * ``` + */ + async getStatus( + federalTaxNumber: string, + birthDate: string | Date + ): Promise { + const normalizedCpf = validateCpf(federalTaxNumber); + const normalizedDate = validateBirthDate(birthDate); + + const response = await this.http.get( + `/v1/naturalperson/status/${normalizedCpf}/${normalizedDate}` + ); + + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Creates a NaturalPersonLookupResource instance + * + * @param http - HTTP client configured for the Natural Person API + * @returns NaturalPersonLookupResource instance + */ +export function createNaturalPersonLookupResource(http: HttpClient): NaturalPersonLookupResource { + return new NaturalPersonLookupResource(http); +} diff --git a/src/core/resources/product-invoice-query.ts b/src/core/resources/product-invoice-query.ts new file mode 100644 index 0000000..396e174 --- /dev/null +++ b/src/core/resources/product-invoice-query.ts @@ -0,0 +1,204 @@ +/** + * NFE.io SDK v3 - Product Invoice Query Resource + * + * Queries NF-e (Nota Fiscal Eletrônica) product invoices directly on SEFAZ + * by access key. Read-only lookups — no company scope required. + * Uses the API host: nfe.api.nfe.io + */ + +import type { HttpClient } from '../http/client.js'; +import type { + ProductInvoiceDetails, + ProductInvoiceEventsResponse, +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Base URL for NF-e Query API */ +export const NFE_QUERY_API_BASE_URL = 'https://nfe.api.nfe.io'; + +/** Regex pattern for valid access key (44 numeric digits) */ +const ACCESS_KEY_PATTERN = /^\d{44}$/; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates access key format (44 numeric digits) + * @param accessKey - The access key to validate + * @throws {ValidationError} If access key is empty or has invalid format + */ +function validateAccessKey(accessKey: string): void { + if (!accessKey || accessKey.trim() === '') { + throw new ValidationError('Access key is required'); + } + + const normalized = accessKey.trim(); + if (!ACCESS_KEY_PATTERN.test(normalized)) { + throw new ValidationError( + `Invalid access key: "${accessKey}". Expected 44 numeric digits.` + ); + } +} + +// ============================================================================ +// Resource Implementation +// ============================================================================ + +/** + * Product Invoice Query Resource + * + * @description + * Queries NF-e (Nota Fiscal Eletrônica) product invoices on SEFAZ by access key. + * This is a read-only resource that does not require company scope. + * + * **Capabilities:** + * - Retrieve full invoice details (issuer, buyer, items, totals, transport, payment) + * - Download DANFE PDF + * - Download NF-e XML + * - List fiscal events (cancellations, corrections, manifestations) + * + * **Authentication:** Uses data API key (`dataApiKey` or `apiKey` fallback). + * + * @example + * ```typescript + * const details = await nfe.productInvoiceQuery.retrieve( + * '35240112345678000190550010000001231234567890' + * ); + * console.log(details.issuer?.name, details.totals?.icms?.invoiceAmount); + * ``` + */ +export class ProductInvoiceQueryResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Public Methods + // -------------------------------------------------------------------------- + + /** + * Retrieve full product invoice (NF-e) details from SEFAZ by access key + * + * @param accessKey - 44-digit numeric access key (Chave de Acesso) + * @returns Full invoice details including issuer, buyer, items, totals, transport, and payment + * @throws {ValidationError} If access key format is invalid + * @throws {NotFoundError} If no invoice matches the access key (HTTP 404) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const invoice = await nfe.productInvoiceQuery.retrieve( + * '35240112345678000190550010000001231234567890' + * ); + * console.log(invoice.currentStatus); // 'authorized' + * console.log(invoice.issuer?.name); + * console.log(invoice.totals?.icms?.invoiceAmount); + * ``` + */ + async retrieve(accessKey: string): Promise { + validateAccessKey(accessKey); + const response = await this.http.get( + `/v2/productinvoices/${accessKey.trim()}` + ); + return response.data; + } + + /** + * Download the DANFE PDF for a product invoice by access key + * + * @param accessKey - 44-digit numeric access key (Chave de Acesso) + * @returns Buffer containing the PDF binary content + * @throws {ValidationError} If access key format is invalid + * @throws {NotFoundError} If no invoice matches the access key (HTTP 404) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const pdfBuffer = await nfe.productInvoiceQuery.downloadPdf( + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('danfe.pdf', pdfBuffer); + * ``` + */ + async downloadPdf(accessKey: string): Promise { + validateAccessKey(accessKey); + const response = await this.http.getBuffer( + `/v2/productinvoices/${accessKey.trim()}.pdf`, + 'application/pdf' + ); + return response.data; + } + + /** + * Download the raw NF-e XML for a product invoice by access key + * + * @param accessKey - 44-digit numeric access key (Chave de Acesso) + * @returns Buffer containing the XML binary content + * @throws {ValidationError} If access key format is invalid + * @throws {NotFoundError} If no invoice matches the access key (HTTP 404) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const xmlBuffer = await nfe.productInvoiceQuery.downloadXml( + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('nfe.xml', xmlBuffer); + * ``` + */ + async downloadXml(accessKey: string): Promise { + validateAccessKey(accessKey); + const response = await this.http.getBuffer( + `/v2/productinvoices/${accessKey.trim()}.xml`, + 'application/xml' + ); + return response.data; + } + + /** + * List fiscal events for a product invoice by access key + * + * Events include cancellations, corrections, manifestations, etc. + * + * @param accessKey - 44-digit numeric access key (Chave de Acesso) + * @returns Events response with an array of fiscal events and query timestamp + * @throws {ValidationError} If access key format is invalid + * @throws {NotFoundError} If no invoice matches the access key (HTTP 404) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const result = await nfe.productInvoiceQuery.listEvents( + * '35240112345678000190550010000001231234567890' + * ); + * for (const event of result.events ?? []) { + * console.log(event.description, event.authorizedOn); + * } + * ``` + */ + async listEvents(accessKey: string): Promise { + validateAccessKey(accessKey); + const response = await this.http.get( + `/v2/productinvoices/events/${accessKey.trim()}` + ); + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a new ProductInvoiceQueryResource instance + */ +export function createProductInvoiceQueryResource(http: HttpClient): ProductInvoiceQueryResource { + return new ProductInvoiceQueryResource(http); +} diff --git a/src/core/resources/product-invoices.ts b/src/core/resources/product-invoices.ts new file mode 100644 index 0000000..234a6cb --- /dev/null +++ b/src/core/resources/product-invoices.ts @@ -0,0 +1,526 @@ +/** + * NFE.io SDK v3 - Product Invoices Resource (NF-e Issuance) + * + * Handles NF-e (Nota Fiscal Eletrônica de Produto) issuance operations via the v2 API. + * Uses api.nfse.io host (same as transportation/inbound resources). + */ + +import type { HttpClient } from '../http/client.js'; +import type { + NfeProductInvoiceIssueData, + NfeProductInvoice, + NfeProductInvoiceListOptions, + NfeProductInvoiceListResponse, + NfeProductInvoiceSubListOptions, + NfeInvoiceItemsResponse, + NfeProductInvoiceEventsResponse, + NfeFileResource, + NfeRequestCancellationResource, + NfeDisablementData, + NfeDisablementResource, +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +function validateCompanyId(companyId: string): void { + if (!companyId || companyId.trim() === '') { + throw new ValidationError('Company ID is required'); + } +} + +function validateInvoiceId(invoiceId: string): void { + if (!invoiceId || invoiceId.trim() === '') { + throw new ValidationError('Invoice ID is required'); + } +} + +function validateStateTaxId(stateTaxId: string): void { + if (!stateTaxId || stateTaxId.trim() === '') { + throw new ValidationError('State tax ID is required'); + } +} + +function buildQueryString(params: Record): string { + const parts: string[] = []; + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + } + return parts.length > 0 ? `?${parts.join('&')}` : ''; +} + +// ============================================================================ +// Product Invoices Resource +// ============================================================================ + +/** + * Product Invoices (NF-e) API Resource + * + * @description + * Provides full lifecycle management for NF-e (Nota Fiscal Eletronica de Produto) + * product invoices -- issue, list, retrieve, cancel, send correction letters (CC-e), + * disable invoice numbers, and download files (PDF/XML). + * + * All operations are scoped by company and use the api.nfse.io v2 API. + * + * **Important:** Issue, cancel, correction letter, and disablement operations are + * asynchronous -- they return 202/204 indicating the request was enqueued. + * Completion is notified via webhooks. + * + * **Prerequisites:** + * - Company must be registered with a valid A1 digital certificate + * - State tax registration (Inscricao Estadual) must be configured + * + * @example Issue a product invoice + * \`\`\`typescript + * const result = await nfe.productInvoices.create('company-id', { + * operationNature: 'Venda de mercadoria', + * operationType: 'Outgoing', + * buyer: { name: 'Empresa LTDA', federalTaxNumber: 12345678000190 }, + * items: [{ code: 'PROD-001', description: 'Produto X', quantity: 1, unitAmount: 100 }], + * payment: [{ paymentDetail: [{ method: 'Cash', amount: 100 }] }], + * }); + * \`\`\` + */ +export class ProductInvoicesResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + private basePath(companyId: string): string { + return `/v2/companies/${companyId}/productinvoices`; + } + + // -------------------------------------------------------------------------- + // Issue (Create) + // -------------------------------------------------------------------------- + + /** + * Issue a product invoice (NF-e) by posting it to the processing queue. + * + * Processing is asynchronous -- a 202 response indicates the invoice was enqueued. + * Monitor completion via webhooks. + * + * @param companyId - The company ID + * @param data - Invoice issue data (buyer, items, payment, operationNature, etc.) + * @returns The enqueued invoice data + * @throws {ValidationError} If companyId is empty + * @throws {BadRequestError} If invoice data is invalid + */ + async create( + companyId: string, + data: NfeProductInvoiceIssueData, + ): Promise { + validateCompanyId(companyId); + const response = await this.http.post( + this.basePath(companyId), + data, + ); + return response.data; + } + + /** + * Issue a product invoice (NF-e) specifying a particular state tax registration. + * + * Processing is asynchronous -- a 202 response indicates the invoice was enqueued. + * + * @param companyId - The company ID + * @param stateTaxId - The state tax registration ID (Inscricao Estadual) + * @param data - Invoice issue data + * @returns The enqueued invoice data + * @throws {ValidationError} If companyId or stateTaxId is empty + */ + async createWithStateTax( + companyId: string, + stateTaxId: string, + data: NfeProductInvoiceIssueData, + ): Promise { + validateCompanyId(companyId); + validateStateTaxId(stateTaxId); + const response = await this.http.post( + `/v2/companies/${companyId}/statetaxes/${stateTaxId}/productinvoices`, + data, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // List & Retrieve + // -------------------------------------------------------------------------- + + /** + * List product invoices (NF-e) for a company with cursor-based pagination. + * + * The environment option is required. + * + * @param companyId - The company ID + * @param options - List options (environment required, pagination, ElasticSearch query) + * @returns Paginated list of invoices + * @throws {ValidationError} If companyId is empty or environment is missing + */ + async list( + companyId: string, + options: NfeProductInvoiceListOptions, + ): Promise { + validateCompanyId(companyId); + if (!options?.environment) { + throw new ValidationError('Environment is required (Production or Test)'); + } + const params: Record = { + environment: options.environment, + }; + if (options.startingAfter !== undefined) params.startingAfter = options.startingAfter; + if (options.endingBefore !== undefined) params.endingBefore = options.endingBefore; + if (options.limit !== undefined) params.limit = options.limit; + if (options.q !== undefined) params.q = options.q; + + const response = await this.http.get( + this.basePath(companyId), + params, + ); + return response.data; + } + + /** + * Retrieve a single product invoice (NF-e) by ID. + * + * Returns full invoice details including authorization, buyer, totals, + * transport, billing, payment, and last events. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @returns Full invoice details + * @throws {ValidationError} If companyId or invoiceId is empty + * @throws {NotFoundError} If invoice does not exist + */ + async retrieve( + companyId: string, + invoiceId: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}`, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Cancel + // -------------------------------------------------------------------------- + + /** + * Cancel a product invoice (NF-e) by enqueuing it for cancellation. + * + * Processing is asynchronous -- a 204 response indicates the request was enqueued. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID to cancel + * @param reason - Optional reason for cancellation + * @returns Cancellation request details + * @throws {ValidationError} If companyId or invoiceId is empty + * @throws {NotFoundError} If invoice does not exist + */ + async cancel( + companyId: string, + invoiceId: string, + reason?: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const params: Record = {}; + if (reason !== undefined) params.reason = reason; + const qs = buildQueryString(params); + const response = await this.http.delete( + `${this.basePath(companyId)}/${invoiceId}${qs}`, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Items & Events + // -------------------------------------------------------------------------- + + /** + * List items (products/services) for a specific invoice. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @param options - Optional pagination (limit, startingAfter) + * @returns Paginated list of invoice items + * @throws {ValidationError} If companyId or invoiceId is empty + * @throws {NotFoundError} If invoice does not exist + */ + async listItems( + companyId: string, + invoiceId: string, + options?: NfeProductInvoiceSubListOptions, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const params: Record = {}; + if (options?.limit !== undefined) params.limit = options.limit; + if (options?.startingAfter !== undefined) params.startingAfter = options.startingAfter; + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/items`, + params, + ); + return response.data; + } + + /** + * List fiscal events for a specific invoice. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @param options - Optional pagination (limit, startingAfter) + * @returns Paginated list of invoice events + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async listEvents( + companyId: string, + invoiceId: string, + options?: NfeProductInvoiceSubListOptions, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const params: Record = {}; + if (options?.limit !== undefined) params.limit = options.limit; + if (options?.startingAfter !== undefined) params.startingAfter = options.startingAfter; + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/events`, + params, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // File Downloads (PDF / XML) + // -------------------------------------------------------------------------- + + /** + * Get the URL for the DANFE PDF file of an invoice. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @param force - If true, forces PDF regeneration regardless of FlowStatus + * @returns File resource with URI to the PDF + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async downloadPdf( + companyId: string, + invoiceId: string, + force?: boolean, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const params: Record = {}; + if (force !== undefined) params.force = force; + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/pdf`, + params, + ); + return response.data; + } + + /** + * Get the URL for the authorized NF-e XML file. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @returns File resource with URI to the XML + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async downloadXml( + companyId: string, + invoiceId: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/xml`, + ); + return response.data; + } + + /** + * Get the URL for the NF-e rejection XML file. + * + * Uses the /xml-rejection endpoint (canonical form). + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @returns File resource with URI to the rejection XML + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async downloadRejectionXml( + companyId: string, + invoiceId: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/xml-rejection`, + ); + return response.data; + } + + /** + * Get the URL for the contingency authorization (EPEC) XML file. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @returns File resource with URI to the EPEC XML + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async downloadEpecXml( + companyId: string, + invoiceId: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/xml-epec`, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Correction Letter (CC-e) + // -------------------------------------------------------------------------- + + /** + * Send a correction letter (Carta de Correcao - CC-e) for a product invoice. + * + * Processing is asynchronous. The reason text must contain between 15 and 1,000 + * characters without accents or special characters. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @param reason - Correction reason (15-1,000 characters, no accents/special chars) + * @returns Cancellation request resource with operation details + * @throws {ValidationError} If reason is too short or too long + */ + async sendCorrectionLetter( + companyId: string, + invoiceId: string, + reason: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + if (!reason || reason.length < 15) { + throw new ValidationError( + 'Correction letter reason must contain at least 15 characters', + ); + } + if (reason.length > 1000) { + throw new ValidationError( + 'Correction letter reason must contain at most 1,000 characters', + ); + } + const response = await this.http.put( + `${this.basePath(companyId)}/${invoiceId}/correctionletter`, + { reason }, + ); + return response.data; + } + + /** + * Get the URL for the CC-e DANFE PDF file. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @returns File resource with URI to the correction letter PDF + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async downloadCorrectionLetterPdf( + companyId: string, + invoiceId: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/correctionletter/pdf`, + ); + return response.data; + } + + /** + * Get the URL for the CC-e XML file. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID + * @returns File resource with URI to the correction letter XML + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async downloadCorrectionLetterXml( + companyId: string, + invoiceId: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const response = await this.http.get( + `${this.basePath(companyId)}/${invoiceId}/correctionletter/xml`, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Disablement (Inutilizacao) + // -------------------------------------------------------------------------- + + /** + * Disable (inutilizar) a specific product invoice by ID. + * + * Processing is asynchronous. The reason parameter is optional. + * + * @param companyId - The company ID + * @param invoiceId - The invoice ID to disable + * @param reason - Optional reason for disablement + * @returns Cancellation request resource + * @throws {ValidationError} If companyId or invoiceId is empty + */ + async disable( + companyId: string, + invoiceId: string, + reason?: string, + ): Promise { + validateCompanyId(companyId); + validateInvoiceId(invoiceId); + const params: Record = {}; + if (reason !== undefined) params.reason = reason; + const qs = buildQueryString(params); + const response = await this.http.post( + `${this.basePath(companyId)}/${invoiceId}/disablement${qs}`, + ); + return response.data; + } + + /** + * Disable a range of invoice numbers for a company. + * + * If disabling a single number, set beginNumber and lastNumber to the same value. + * + * @param companyId - The company ID + * @param data - Disablement data (environment, serie, state, beginNumber, lastNumber, reason?) + * @returns Disablement resource with operation details + * @throws {ValidationError} If companyId is empty + */ + async disableRange( + companyId: string, + data: NfeDisablementData, + ): Promise { + validateCompanyId(companyId); + const response = await this.http.post( + `${this.basePath(companyId)}/disablement`, + data, + ); + return response.data; + } +} diff --git a/src/core/resources/state-taxes.ts b/src/core/resources/state-taxes.ts new file mode 100644 index 0000000..1d46935 --- /dev/null +++ b/src/core/resources/state-taxes.ts @@ -0,0 +1,268 @@ +/** + * NFE.io SDK v3 - State Taxes Resource (Inscrições Estaduais) + * + * Handles CRUD operations for company state tax registrations (Inscrições Estaduais) + * via the api.nfse.io v2 API. State taxes define the series, numbering, environment, + * and state code configuration required for NF-e issuance. + */ + +import type { HttpClient } from '../http/client.js'; +import type { + NfeStateTax, + NfeStateTaxCreateData, + NfeStateTaxUpdateData, + NfeStateTaxListResponse, + NfeStateTaxListOptions, +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates company ID is not empty. + * @param companyId - The company ID to validate + * @throws {ValidationError} If company ID is empty + */ +function validateCompanyId(companyId: string): void { + if (!companyId || companyId.trim() === '') { + throw new ValidationError('Company ID is required'); + } +} + +/** + * Validates state tax ID is not empty. + * @param stateTaxId - The state tax ID to validate + * @throws {ValidationError} If state tax ID is empty + */ +function validateStateTaxId(stateTaxId: string): void { + if (!stateTaxId || stateTaxId.trim() === '') { + throw new ValidationError('State tax ID is required'); + } +} + +// ============================================================================ +// State Taxes Resource +// ============================================================================ + +/** + * State Taxes (Inscrições Estaduais) API Resource + * + * @description + * Provides CRUD operations for company state tax registrations. + * State taxes define the series, numbering, environment, and state configuration + * required for NF-e product invoice issuance. + * + * All operations are scoped by company and use the `api.nfse.io` v2 API. + * + * @example List state taxes + * ```typescript + * const result = await nfe.stateTaxes.list('company-id'); + * for (const tax of result.stateTaxes ?? []) { + * console.log(tax.code, tax.taxNumber, tax.status); + * } + * ``` + * + * @example Create a state tax registration + * ```typescript + * const tax = await nfe.stateTaxes.create('company-id', { + * taxNumber: '123456789', + * serie: 1, + * number: 1, + * code: 'sP', + * environmentType: 'production', + * type: 'nFe', + * }); + * console.log(tax.id); + * ``` + * + * @example Update and delete + * ```typescript + * await nfe.stateTaxes.update('company-id', 'state-tax-id', { serie: 2 }); + * await nfe.stateTaxes.delete('company-id', 'state-tax-id'); + * ``` + */ +export class StateTaxesResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + /** + * Returns the base path for state tax operations. + */ + private basePath(companyId: string): string { + return `/v2/companies/${companyId}/statetaxes`; + } + + // -------------------------------------------------------------------------- + // List + // -------------------------------------------------------------------------- + + /** + * List all state tax registrations (Inscrições Estaduais) for a company. + * + * Uses cursor-based pagination with `startingAfter`, `endingBefore`, and `limit`. + * + * @param companyId - The company ID + * @param options - Optional pagination options + * @returns List of state tax registrations + * @throws {ValidationError} If companyId is empty + * + * @example + * ```typescript + * const result = await nfe.stateTaxes.list('company-id'); + * for (const tax of result.stateTaxes ?? []) { + * console.log(tax.id, tax.taxNumber, tax.serie, tax.status); + * } + * ``` + */ + async list( + companyId: string, + options?: NfeStateTaxListOptions, + ): Promise { + validateCompanyId(companyId); + const params: Record = {}; + if (options?.startingAfter !== undefined) params.startingAfter = options.startingAfter; + if (options?.endingBefore !== undefined) params.endingBefore = options.endingBefore; + if (options?.limit !== undefined) params.limit = options.limit; + const response = await this.http.get( + this.basePath(companyId), + params, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Create + // -------------------------------------------------------------------------- + + /** + * Create a new state tax registration (Inscrição Estadual) for a company. + * + * @param companyId - The company ID + * @param data - State tax data (taxNumber, serie, and number are required) + * @returns The created state tax record + * @throws {ValidationError} If companyId is empty + * @throws {BadRequestError} If required fields are missing + * + * @example + * ```typescript + * const tax = await nfe.stateTaxes.create('company-id', { + * taxNumber: '123456789', + * serie: 1, + * number: 1, + * code: 'sP', + * environmentType: 'production', + * }); + * ``` + */ + async create( + companyId: string, + data: NfeStateTaxCreateData, + ): Promise { + validateCompanyId(companyId); + const response = await this.http.post( + this.basePath(companyId), + { stateTax: data }, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Retrieve + // -------------------------------------------------------------------------- + + /** + * Retrieve a specific state tax registration by ID. + * + * @param companyId - The company ID + * @param stateTaxId - The state tax ID + * @returns The state tax record + * @throws {ValidationError} If companyId or stateTaxId is empty + * @throws {NotFoundError} If state tax record does not exist + * + * @example + * ```typescript + * const tax = await nfe.stateTaxes.retrieve('company-id', 'state-tax-id'); + * console.log(tax.taxNumber, tax.environmentType, tax.serie); + * ``` + */ + async retrieve( + companyId: string, + stateTaxId: string, + ): Promise { + validateCompanyId(companyId); + validateStateTaxId(stateTaxId); + const response = await this.http.get( + `${this.basePath(companyId)}/${stateTaxId}`, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Update + // -------------------------------------------------------------------------- + + /** + * Update an existing state tax registration. + * + * @param companyId - The company ID + * @param stateTaxId - The state tax ID to update + * @param data - Fields to update + * @returns The updated state tax record + * @throws {ValidationError} If companyId or stateTaxId is empty + * @throws {NotFoundError} If state tax record does not exist + * + * @example + * ```typescript + * const tax = await nfe.stateTaxes.update('company-id', 'state-tax-id', { + * serie: 2, + * environmentType: 'test', + * }); + * ``` + */ + async update( + companyId: string, + stateTaxId: string, + data: NfeStateTaxUpdateData, + ): Promise { + validateCompanyId(companyId); + validateStateTaxId(stateTaxId); + const response = await this.http.put( + `${this.basePath(companyId)}/${stateTaxId}`, + { stateTax: data }, + ); + return response.data; + } + + // -------------------------------------------------------------------------- + // Delete + // -------------------------------------------------------------------------- + + /** + * Delete a state tax registration. + * + * @param companyId - The company ID + * @param stateTaxId - The state tax ID to delete + * @throws {ValidationError} If companyId or stateTaxId is empty + * @throws {NotFoundError} If state tax record does not exist + * + * @example + * ```typescript + * await nfe.stateTaxes.delete('company-id', 'state-tax-id'); + * ``` + */ + async delete( + companyId: string, + stateTaxId: string, + ): Promise { + validateCompanyId(companyId); + validateStateTaxId(stateTaxId); + await this.http.delete( + `${this.basePath(companyId)}/${stateTaxId}`, + ); + } +} diff --git a/src/core/resources/tax-calculation.ts b/src/core/resources/tax-calculation.ts new file mode 100644 index 0000000..4ec6e4b --- /dev/null +++ b/src/core/resources/tax-calculation.ts @@ -0,0 +1,194 @@ +/** + * NFE.io SDK v3 - Tax Calculation Resource + * + * Provides access to the Motor de Cálculo de Tributos (Tax Calculation Engine), + * which computes all applicable Brazilian taxes (ICMS, ICMS-ST, PIS, COFINS, + * IPI, II) for product operations based on fiscal context. + * + * Uses the API host: api.nfse.io + * + * @see https://nfe.io/docs/nota-fiscal-eletronica/motor-de-calculo-de-imposto/ + */ + +import type { HttpClient } from '../http/client.js'; +import type { CalculateRequest, CalculateResponse } from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates that tenantId is a non-empty string. + * @param tenantId - The tenant/subscription ID to validate + * @throws {ValidationError} If tenantId is empty or not a string + */ +function validateTenantId(tenantId: string): void { + if (!tenantId || typeof tenantId !== 'string' || tenantId.trim() === '') { + throw new ValidationError('tenantId is required and must be a non-empty string'); + } +} + +/** + * Validates required fields on a CalculateRequest. + * @param request - The request payload to validate + * @throws {ValidationError} If required fields are missing or invalid + */ +function validateCalculateRequest(request: CalculateRequest): void { + if (!request) { + throw new ValidationError('request is required'); + } + + if (!request.issuer) { + throw new ValidationError('request.issuer is required'); + } + + if (!request.recipient) { + throw new ValidationError('request.recipient is required'); + } + + if (!request.operationType) { + throw new ValidationError('request.operationType is required'); + } + + if (!request.items || !Array.isArray(request.items) || request.items.length === 0) { + throw new ValidationError('request.items is required and must be a non-empty array'); + } +} + +// ============================================================================ +// Resource Implementation +// ============================================================================ + +/** + * Tax Calculation Resource + * + * @description + * Provides access to the NFE.io Tax Calculation Engine (Motor de Cálculo de + * Tributos). The engine computes all applicable Brazilian taxes for product + * operations, returning per-item tax breakdowns including CFOP determination. + * + * **Supported taxes:** + * - ICMS (including ICMS-ST and FCP) + * - ICMS interestadual (DIFAL / UF destination) + * - PIS + * - COFINS + * - IPI + * - II (Import Tax) + * + * **Authentication:** Uses data API key (`dataApiKey` or `apiKey` fallback) + * via the CTE HTTP client (`api.nfse.io`). + * + * @example + * ```typescript + * const result = await nfe.taxCalculation.calculate('my-tenant-id', { + * operationType: 'Outgoing', + * issuer: { state: 'SP', taxRegime: 'RealProfit' }, + * recipient: { state: 'RJ' }, + * items: [{ + * id: '1', + * operationCode: 121, + * origin: 'National', + * quantity: 10, + * unitAmount: 100.00, + * ncm: '61091000' + * }] + * }); + * + * for (const item of result.items ?? []) { + * console.log(`Item ${item.id}: CFOP ${item.cfop}`); + * console.log(` ICMS CST: ${item.icms?.cst}, value: ${item.icms?.vICMS}`); + * console.log(` PIS CST: ${item.pis?.cst}, value: ${item.pis?.vPIS}`); + * } + * ``` + */ +export class TaxCalculationResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Public Methods + // -------------------------------------------------------------------------- + + /** + * Calculate taxes for a product operation + * + * @description + * Submits an operation with issuer, recipient, operation type, and product items + * to the Tax Calculation Engine. Returns per-item tax breakdowns including all + * applicable Brazilian taxes (ICMS, PIS, COFINS, IPI, II) with CFOP determination. + * + * The `tenantId` is the subscription/account identifier that scopes the tax rules. + * + * @param tenantId - Subscription/account ID (required, non-empty) + * @param request - Tax calculation request with issuer, recipient, operation type, and items + * @returns Tax calculation response with per-item breakdowns + * @throws {ValidationError} If tenantId is empty + * @throws {ValidationError} If required request fields are missing (issuer, recipient, operationType, items) + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * @throws {BadRequestError} If the API rejects the payload (HTTP 400) + * @throws {ValidationError} If the API returns unprocessable content (HTTP 422) + * + * @example Basic calculation + * ```typescript + * const result = await nfe.taxCalculation.calculate('tenant-123', { + * operationType: 'Outgoing', + * issuer: { state: 'SP', taxRegime: 'RealProfit' }, + * recipient: { state: 'RJ' }, + * items: [{ + * id: 'item-1', + * operationCode: 121, + * origin: 'National', + * quantity: 1, + * unitAmount: 500.00, + * ncm: '61091000' + * }] + * }); + * console.log(result.items?.[0]?.cfop); // e.g., 6102 + * ``` + * + * @example With per-item tax profiles + * ```typescript + * const result = await nfe.taxCalculation.calculate('tenant-123', { + * operationType: 'Incoming', + * issuer: { state: 'MG', taxRegime: 'NationalSimple' }, + * recipient: { state: 'SP', taxRegime: 'RealProfit' }, + * items: [{ + * id: 'item-1', + * operationCode: 569, + * acquisitionPurpose: '569', + * origin: 'National', + * quantity: 100, + * unitAmount: 25.50, + * ncm: '39174090', + * issuerTaxProfile: 'industry', + * recipientTaxProfile: 'industry' + * }] + * }); + * ``` + */ + async calculate(tenantId: string, request: CalculateRequest): Promise { + validateTenantId(tenantId); + validateCalculateRequest(request); + + const response = await this.http.post( + `/tax-rules/${encodeURIComponent(tenantId.trim())}/engine/calculate`, + request + ); + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a new TaxCalculationResource instance + */ +export function createTaxCalculationResource(http: HttpClient): TaxCalculationResource { + return new TaxCalculationResource(http); +} diff --git a/src/core/resources/tax-codes.ts b/src/core/resources/tax-codes.ts new file mode 100644 index 0000000..5ca9fd5 --- /dev/null +++ b/src/core/resources/tax-codes.ts @@ -0,0 +1,214 @@ +/** + * NFE.io SDK v3 - Tax Codes Resource + * + * Provides paginated listings of auxiliary tax code reference tables + * needed as inputs for tax calculation: operation codes, acquisition + * purposes, issuer tax profiles, and recipient tax profiles. + * + * Uses the API host: api.nfse.io + * + * @see https://nfe.io/docs/nota-fiscal-eletronica/motor-de-calculo-de-imposto/ + */ + +import type { HttpClient } from '../http/client.js'; +import type { TaxCodePaginatedResponse, TaxCodeListOptions } from '../types.js'; + +// ============================================================================ +// Helpers +// ============================================================================ + +/** + * Build query string from pagination options. + * @param options - Optional pagination parameters + * @returns Query string (including leading '?') or empty string + */ +function buildPaginationQuery(options?: TaxCodeListOptions): string { + if (!options) return ''; + + const params = new URLSearchParams(); + + if (options.pageIndex !== undefined && options.pageIndex !== null) { + params.set('pageIndex', String(options.pageIndex)); + } + if (options.pageCount !== undefined && options.pageCount !== null) { + params.set('pageCount', String(options.pageCount)); + } + + const qs = params.toString(); + return qs ? `?${qs}` : ''; +} + +// ============================================================================ +// Resource Implementation +// ============================================================================ + +/** + * Tax Codes Resource + * + * @description + * Provides paginated listings of the four auxiliary reference tables used as + * inputs for the Tax Calculation Engine: + * + * - **Operation Codes** — natureza de operação (e.g., 121 = "Venda de mercadoria") + * - **Acquisition Purposes** — finalidade de aquisição (e.g., 569 = "Compra para comercialização") + * - **Issuer Tax Profiles** — perfil fiscal do emissor (e.g., "retail", "industry") + * - **Recipient Tax Profiles** — perfil fiscal do destinatário (e.g., "final_consumer_non_icms_contributor") + * + * All methods support pagination via `pageIndex` (1-based) and `pageCount` parameters. + * + * **Authentication:** Uses data API key (`dataApiKey` or `apiKey` fallback) + * via the CTE HTTP client (`api.nfse.io`). + * + * @example + * ```typescript + * // List operation codes (first page) + * const codes = await nfe.taxCodes.listOperationCodes(); + * for (const code of codes.items ?? []) { + * console.log(`${code.code} - ${code.description}`); + * } + * + * // With pagination + * const page2 = await nfe.taxCodes.listOperationCodes({ pageIndex: 2, pageCount: 20 }); + * console.log(`Page ${page2.currentPage} of ${page2.totalPages}`); + * ``` + */ +export class TaxCodesResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Public Methods + // -------------------------------------------------------------------------- + + /** + * List operation codes (natureza de operação) + * + * @description + * Returns a paginated list of operation codes used in the `operationCode` field + * of tax calculation item requests. Each code represents a specific operation + * nature (e.g., sale, return, transfer). + * + * @param options - Optional pagination parameters + * @returns Paginated list of operation codes + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const result = await nfe.taxCodes.listOperationCodes(); + * console.log(`Total: ${result.totalCount} codes`); + * for (const code of result.items ?? []) { + * console.log(`${code.code} - ${code.description}`); + * } + * ``` + * + * @example With pagination + * ```typescript + * const page = await nfe.taxCodes.listOperationCodes({ pageIndex: 2, pageCount: 10 }); + * console.log(`Page ${page.currentPage} of ${page.totalPages}`); + * ``` + */ + async listOperationCodes(options?: TaxCodeListOptions): Promise { + const qs = buildPaginationQuery(options); + const response = await this.http.get( + `/tax-codes/operation-code${qs}` + ); + return response.data; + } + + /** + * List acquisition purposes (finalidade de aquisição) + * + * @description + * Returns a paginated list of acquisition purpose codes used in the + * `acquisitionPurpose` field of tax calculation item requests. + * + * @param options - Optional pagination parameters + * @returns Paginated list of acquisition purposes + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const result = await nfe.taxCodes.listAcquisitionPurposes(); + * for (const purpose of result.items ?? []) { + * console.log(`${purpose.code} - ${purpose.description}`); + * } + * ``` + */ + async listAcquisitionPurposes(options?: TaxCodeListOptions): Promise { + const qs = buildPaginationQuery(options); + const response = await this.http.get( + `/tax-codes/acquisition-purpose${qs}` + ); + return response.data; + } + + /** + * List issuer tax profiles (perfil fiscal do emissor) + * + * @description + * Returns a paginated list of issuer tax profile codes used in the + * `issuerTaxProfile` field of tax calculation item requests or the + * `taxProfile` field of the issuer. + * + * @param options - Optional pagination parameters + * @returns Paginated list of issuer tax profiles + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const result = await nfe.taxCodes.listIssuerTaxProfiles(); + * for (const profile of result.items ?? []) { + * console.log(`${profile.code} - ${profile.description}`); + * } + * ``` + */ + async listIssuerTaxProfiles(options?: TaxCodeListOptions): Promise { + const qs = buildPaginationQuery(options); + const response = await this.http.get( + `/tax-codes/issuer-tax-profile${qs}` + ); + return response.data; + } + + /** + * List recipient tax profiles (perfil fiscal do destinatário) + * + * @description + * Returns a paginated list of recipient tax profile codes used in the + * `recipientTaxProfile` field of tax calculation item requests or the + * `taxProfile` field of the recipient. + * + * @param options - Optional pagination parameters + * @returns Paginated list of recipient tax profiles + * @throws {AuthenticationError} If API key is invalid (HTTP 401) + * + * @example + * ```typescript + * const result = await nfe.taxCodes.listRecipientTaxProfiles(); + * for (const profile of result.items ?? []) { + * console.log(`${profile.code} - ${profile.description}`); + * } + * ``` + */ + async listRecipientTaxProfiles(options?: TaxCodeListOptions): Promise { + const qs = buildPaginationQuery(options); + const response = await this.http.get( + `/tax-codes/recipient-tax-profile${qs}` + ); + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a new TaxCodesResource instance + */ +export function createTaxCodesResource(http: HttpClient): TaxCodesResource { + return new TaxCodesResource(http); +} diff --git a/src/core/resources/transportation-invoices.ts b/src/core/resources/transportation-invoices.ts new file mode 100644 index 0000000..1093696 --- /dev/null +++ b/src/core/resources/transportation-invoices.ts @@ -0,0 +1,397 @@ +/** + * NFE.io SDK v3 - Transportation Invoices Resource + * + * Handles CT-e (Conhecimento de Transporte Eletrônico) operations via Distribuição DFe + * Uses a separate API host: api.nfse.io + */ + +import type { HttpClient } from '../http/client.js'; +import type { + TransportationInvoiceInboundSettings, + TransportationInvoiceMetadata, + EnableTransportationInvoiceOptions +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Base URL for CT-e API */ +export const CTE_API_BASE_URL = 'https://api.nfse.io'; + +/** Regex pattern for valid access key (44 numeric digits) */ +const ACCESS_KEY_PATTERN = /^\d{44}$/; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates access key format (44 numeric digits) + * @param accessKey - The CT-e access key to validate + * @throws {ValidationError} If access key format is invalid + */ +function validateAccessKey(accessKey: string): void { + if (!accessKey || accessKey.trim() === '') { + throw new ValidationError('Access key is required'); + } + + const normalized = accessKey.trim(); + if (!ACCESS_KEY_PATTERN.test(normalized)) { + throw new ValidationError( + `Invalid access key: "${accessKey}". Expected 44 numeric digits.` + ); + } +} + +/** + * Validates company ID is not empty + * @param companyId - The company ID to validate + * @throws {ValidationError} If company ID is empty + */ +function validateCompanyId(companyId: string): void { + if (!companyId || companyId.trim() === '') { + throw new ValidationError('Company ID is required'); + } +} + +// ============================================================================ +// Transportation Invoices Resource +// ============================================================================ + +/** + * Transportation Invoices (CT-e) API Resource + * + * @description + * Provides operations for managing CT-e (Conhecimento de Transporte Eletrônico) + * documents via SEFAZ Distribuição DFe. This allows companies to automatically + * receive CT-e documents destined to them. + * + * **Prerequisites:** + * - Company must be registered with a valid A1 digital certificate + * - Webhook must be configured to receive CT-e notifications + * + * **Note:** This resource uses a different API host (api.nfse.io) and may require + * a separate API key configured via `dataApiKey` in the client configuration. + * If not set, it falls back to `apiKey`. + * + * @example Enable automatic CT-e search + * ```typescript + * // Enable with default settings + * const settings = await nfe.transportationInvoices.enable('company-id'); + * + * // Enable starting from a specific NSU + * const settings = await nfe.transportationInvoices.enable('company-id', { + * startFromNsu: 12345 + * }); + * ``` + * + * @example Retrieve CT-e by access key + * ```typescript + * const cte = await nfe.transportationInvoices.retrieve( + * 'company-id', + * '35240112345678000190570010000001231234567890' + * ); + * console.log(cte.nameSender, cte.totalInvoiceAmount); + * ``` + * + * @example Download CT-e XML + * ```typescript + * const xml = await nfe.transportationInvoices.downloadXml( + * 'company-id', + * '35240112345678000190570010000001231234567890' + * ); + * // Save to file or parse as needed + * ``` + */ +export class TransportationInvoicesResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Automatic Search Management + // -------------------------------------------------------------------------- + + /** + * Enable automatic CT-e search for a company + * + * Activates the automatic search for CT-e documents destined to the specified + * company via SEFAZ Distribuição DFe. Once enabled, new CT-es will be automatically + * retrieved and can be accessed via the configured webhook. + * + * @param companyId - The company ID to enable automatic search for + * @param options - Optional settings for the automatic search + * @returns Promise with the inbound settings after enabling + * @throws {ValidationError} If company ID is empty + * @throws {BadRequestError} If the request is invalid + * @throws {NotFoundError} If the company is not found + * + * @example + * ```typescript + * // Enable with default settings + * const settings = await nfe.transportationInvoices.enable('company-id'); + * + * // Enable starting from a specific NSU + * const settings = await nfe.transportationInvoices.enable('company-id', { + * startFromNsu: 12345 + * }); + * + * // Enable starting from a specific date + * const settings = await nfe.transportationInvoices.enable('company-id', { + * startFromDate: '2024-01-01T00:00:00Z' + * }); + * ``` + */ + async enable( + companyId: string, + options?: EnableTransportationInvoiceOptions + ): Promise { + validateCompanyId(companyId); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/transportationinvoices`, + options || {} + ); + + return response.data; + } + + /** + * Disable automatic CT-e search for a company + * + * Deactivates the automatic search for CT-e documents. After disabling, + * no new CT-es will be retrieved for the company. + * + * @param companyId - The company ID to disable automatic search for + * @returns Promise with the inbound settings after disabling + * @throws {ValidationError} If company ID is empty + * @throws {NotFoundError} If automatic search is not enabled for this company + * + * @example + * ```typescript + * const settings = await nfe.transportationInvoices.disable('company-id'); + * console.log('Automatic search disabled:', settings.status); + * ``` + */ + async disable(companyId: string): Promise { + validateCompanyId(companyId); + + const response = await this.http.delete( + `/v2/companies/${companyId}/inbound/transportationinvoices` + ); + + return response.data; + } + + /** + * Get current automatic CT-e search settings + * + * Retrieves the current configuration for automatic CT-e search, + * including status, start NSU, start date, and timestamps. + * + * @param companyId - The company ID to get settings for + * @returns Promise with the current inbound settings + * @throws {ValidationError} If company ID is empty + * @throws {NotFoundError} If automatic search is not configured for this company + * + * @example + * ```typescript + * const settings = await nfe.transportationInvoices.getSettings('company-id'); + * console.log('Status:', settings.status); + * console.log('Start NSU:', settings.startFromNsu); + * console.log('Created:', settings.createdOn); + * ``` + */ + async getSettings(companyId: string): Promise { + validateCompanyId(companyId); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/transportationinvoices` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // CT-e Document Operations + // -------------------------------------------------------------------------- + + /** + * Retrieve CT-e metadata by access key + * + * Gets the metadata of a CT-e document by its 44-digit access key. + * + * @param companyId - The company ID that received the CT-e + * @param accessKey - The 44-digit CT-e access key + * @returns Promise with the CT-e metadata + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the CT-e is not found + * + * @example + * ```typescript + * const cte = await nfe.transportationInvoices.retrieve( + * 'company-id', + * '35240112345678000190570010000001231234567890' + * ); + * console.log('Sender:', cte.nameSender); + * console.log('CNPJ:', cte.federalTaxNumberSender); + * console.log('Amount:', cte.totalInvoiceAmount); + * console.log('Issued:', cte.issuedOn); + * ``` + */ + async retrieve( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}` + ); + + return response.data; + } + + /** + * Download CT-e XML by access key + * + * Gets the XML content of a CT-e document. + * + * @param companyId - The company ID that received the CT-e + * @param accessKey - The 44-digit CT-e access key + * @returns Promise with the XML content as a string + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the CT-e is not found + * + * @example + * ```typescript + * const xml = await nfe.transportationInvoices.downloadXml( + * 'company-id', + * '35240112345678000190570010000001231234567890' + * ); + * + * // Save to file + * fs.writeFileSync('cte.xml', xml); + * + * // Or parse with an XML library + * const parsed = parseXml(xml); + * ``` + */ + async downloadXml(companyId: string, accessKey: string): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/xml` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // CT-e Event Operations + // -------------------------------------------------------------------------- + + /** + * Retrieve CT-e event metadata + * + * Gets the metadata of an event related to a CT-e document. + * + * @param companyId - The company ID that received the CT-e + * @param accessKey - The 44-digit CT-e access key + * @param eventKey - The event key + * @returns Promise with the event metadata + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const event = await nfe.transportationInvoices.getEvent( + * 'company-id', + * '35240112345678000190570010000001231234567890', + * 'event-key-123' + * ); + * console.log('Event:', event.description); + * ``` + */ + async getEvent( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + if (!eventKey || eventKey.trim() === '') { + throw new ValidationError('Event key is required'); + } + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/events/${eventKey.trim()}` + ); + + return response.data; + } + + /** + * Download CT-e event XML + * + * Gets the XML content of a CT-e event. + * + * @param companyId - The company ID that received the CT-e + * @param accessKey - The 44-digit CT-e access key + * @param eventKey - The event key + * @returns Promise with the event XML content as a string + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const xml = await nfe.transportationInvoices.downloadEventXml( + * 'company-id', + * '35240112345678000190570010000001231234567890', + * 'event-key-123' + * ); + * fs.writeFileSync('cte-event.xml', xml); + * ``` + */ + async downloadEventXml( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + if (!eventKey || eventKey.trim() === '') { + throw new ValidationError('Event key is required'); + } + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/events/${eventKey.trim()}/xml` + ); + + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Creates a TransportationInvoicesResource instance + * + * @param http - HTTP client configured for the CT-e API + * @returns TransportationInvoicesResource instance + */ +export function createTransportationInvoicesResource( + http: HttpClient +): TransportationInvoicesResource { + return new TransportationInvoicesResource(http); +} diff --git a/src/core/types.ts b/src/core/types.ts index e983115..3400897 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -17,8 +17,8 @@ export interface NfeConfig { /** NFE.io API Key for main resources (companies, invoices, etc.) */ apiKey?: string; - /** NFE.io API Key specifically for Address API (optional, falls back to apiKey) */ - addressApiKey?: string; + /** NFE.io API Key for data/query services: Addresses, CT-e, CNPJ, CPF (optional, falls back to apiKey) */ + dataApiKey?: string; /** Environment to use (both use same endpoint, differentiated by API key) */ environment?: 'production' | 'development'; /** Custom base URL (overrides environment) */ @@ -294,10 +294,10 @@ export interface PollOptions { * API keys remain optional since validation is done lazily when resources are accessed. */ export interface RequiredNfeConfig { - /** Main API key (may be undefined if only using address API) */ + /** Main API key (may be undefined if only using data services) */ apiKey: string | undefined; - /** Address API key (may be undefined, will fallback to apiKey) */ - addressApiKey: string | undefined; + /** Data API key for query services: Addresses, CT-e, CNPJ, CPF (may be undefined, will fallback to apiKey) */ + dataApiKey: string | undefined; /** Environment */ environment: 'production' | 'development'; /** Base URL for main API */ @@ -394,3 +394,2915 @@ export type NaturalPerson = { name: string; [key: string]: unknown; }; + +// ============================================================================ +// CT-e (Transportation Invoice) Types +// ============================================================================ + +// Import the components type from generated spec +import type { components as CteComponents } from '../generated/consulta-cte-v2.js'; + +/** + * Transportation Invoice inbound settings + * Configuration for automatic CT-e search via SEFAZ Distribuição DFe + */ +export type TransportationInvoiceInboundSettings = + CteComponents['schemas']['DFe.NetCore.Domain.Resources.TransportationInvoiceInboundResource']; + +/** + * Transportation Invoice metadata + * Metadata of a CT-e document retrieved via Distribuição DFe + */ +export type TransportationInvoiceMetadata = + CteComponents['schemas']['DFe.NetCore.Domain.Resources.MetadataResource']; + +/** + * Options for enabling automatic CT-e search + */ +export interface EnableTransportationInvoiceOptions { + /** Start from a specific NSU (Número Sequencial Único) */ + startFromNsu?: number; + /** Start from a specific date (ISO 8601 format) */ + startFromDate?: string; +} + +/** + * CT-e entity status + */ +export type TransportationInvoiceEntityStatus = + CteComponents['schemas']['DFe.NetCore.Domain.Enums.EntityStatus']; + +/** + * CT-e metadata resource type + */ +export type TransportationInvoiceMetadataType = + CteComponents['schemas']['DFe.NetCore.Domain.Enums.MetadataResourceType']; + +// ============================================================================ +// Inbound NF-e Distribution Types +// ============================================================================ + +/** + * Company reference in inbound document metadata + */ +export interface InboundCompany { + /** Company ID */ + id: string; + /** Company CNPJ */ + federalTaxNumber: string; +} + +/** + * Issuer reference in inbound document metadata + */ +export interface InboundIssuer { + /** Issuer CNPJ */ + federalTaxNumber: string; + /** Issuer name */ + name: string; +} + +/** + * Buyer reference in inbound document metadata + */ +export interface InboundBuyer { + /** Buyer CNPJ/CPF */ + federalTaxNumber: string; + /** Buyer name */ + name: string; +} + +/** + * Transportation entity reference in inbound document metadata + */ +export interface InboundTransportation { + /** Transportation CNPJ */ + federalTaxNumber: string; + /** Transportation name */ + name: string; +} + +/** + * Document download links + */ +export interface InboundLinks { + /** XML download URL */ + xml: string; + /** PDF download URL */ + pdf: string; +} + +/** + * Product invoice reference (used in webhook v2 responses) + */ +export interface InboundProductInvoice { + /** Access key of the referenced product invoice */ + accessKey: string; +} + +/** + * Automatic manifesting configuration + */ +export interface AutomaticManifesting { + /** Minutes to wait before automatic awareness operation */ + minutesToWaitAwarenessOperation: string; +} + +/** + * Inbound invoice metadata (webhook v1 format) + * + * Contains details of an NF-e or CT-e document retrieved via Distribuição DFe. + * Corresponds to the generic endpoint `GET /{access_key}`. + */ +export interface InboundInvoiceMetadata { + /** Document ID */ + id: string; + /** Creation timestamp */ + createdOn: string; + /** 44-digit access key */ + accessKey: string; + /** Parent document access key (for events) */ + parentAccessKey: string; + /** Company that received the document */ + company: InboundCompany; + /** Document issuer */ + issuer: InboundIssuer; + /** Document buyer */ + buyer: InboundBuyer; + /** Transportation entity */ + transportation: InboundTransportation; + /** Download links */ + links: InboundLinks; + /** XML download URL */ + xmlUrl: string; + /** Sender CNPJ */ + federalTaxNumberSender: string; + /** Sender name */ + nameSender: string; + /** Document type */ + type: string | null; + /** NSU (Número Sequencial Único) */ + nsu: string; + /** Parent NSU */ + nsuParent: string; + /** NF-e number */ + nfeNumber: string; + /** NF-e serial number */ + nfeSerialNumber: string; + /** Issue date */ + issuedOn: string; + /** Document description */ + description: string; + /** Total invoice amount */ + totalInvoiceAmount: string; + /** Operation type */ + operationType: string | null; +} + +/** + * Inbound product invoice metadata (webhook v2 format) + * + * Extends the base metadata with product invoice references. + * Corresponds to the `GET /productinvoice/{access_key}` endpoint. + */ +export interface InboundProductInvoiceMetadata extends Omit { + /** Referenced product invoices */ + productInvoices: InboundProductInvoice[]; +} + +/** + * Inbound NF-e distribution service settings + * + * Configuration for automatic NF-e search via SEFAZ Distribuição DFe. + */ +export interface InboundSettings { + /** Starting NSU for document retrieval */ + startFromNsu: string; + /** Starting date for document retrieval */ + startFromDate: string; + /** SEFAZ environment (e.g., Production) */ + environmentSEFAZ: string | null; + /** Automatic manifesting configuration */ + automaticManifesting: AutomaticManifesting; + /** Webhook version */ + webhookVersion: string; + /** Company ID */ + companyId: string; + /** Service status */ + status: string | null; + /** Creation timestamp */ + createdOn: string; + /** Last modification timestamp */ + modifiedOn: string; +} + +/** + * Options for enabling automatic NF-e distribution fetch + */ +export interface EnableInboundOptions { + /** Starting NSU number */ + startFromNsu?: string; + /** Starting date (ISO 8601 format) */ + startFromDate?: string; + /** SEFAZ environment */ + environmentSEFAZ?: string; + /** Automatic manifesting settings */ + automaticManifesting?: AutomaticManifesting; + /** Webhook version */ + webhookVersion?: string; +} + +/** + * Manifest event types for Manifestação do Destinatário + * + * - `210210` — Ciência da Operação (awareness of the operation) + * - `210220` — Confirmação da Operação (confirmation of the operation) + * - `210240` — Operação não Realizada (operation not performed) + */ +export type ManifestEventType = 210210 | 210220 | 210240; + +// ============================================================================ +// Product Invoice Query Types (consulta-nf) +// ============================================================================ + +// Enum string unions +// ---------------------------------------------------------------------------- + +/** Current status of a product invoice (NF-e) */ +export type ProductInvoiceStatus = 'unknown' | 'authorized' | 'canceled'; + +/** Payment type indicator */ +export type ProductInvoicePaymentType = 'inCash' | 'term' | 'others'; + +/** Operation type (incoming/outgoing) */ +export type ProductInvoiceOperationType = 'incoming' | 'outgoing'; + +/** Destination of the operation */ +export type ProductInvoiceDestination = 'international_Operation' | 'interstate_Operation' | 'internal_Operation'; + +/** DANFE print format */ +export type ProductInvoicePrintType = 'none' | 'nFeNormalPortrait' | 'nFeNormalLandscape' | 'nFeSimplified' | 'dANFE_NFC_E' | 'dANFE_NFC_E_MSG_ELETRONICA'; + +/** Invoice issue type (emission contingency modes) */ +export type ProductInvoiceIssueType = 'normal' | 'cONTINGENCIA_OFF_LINE_NFC_E' | 'cONTINGENCIA_SVC_RS' | 'cONTINGENCIA_SVC_AN' | 'cONTINGENCIA_FS_DA' | 'cONTINGENCIA_DPEC' | 'cONTINGENCIA_SCAN' | 'cONTINGENCIA_FS_IA'; + +/** Environment type */ +export type ProductInvoiceEnvironmentType = 'production' | 'test'; + +/** Invoice purpose */ +export type ProductInvoicePurposeType = 'normal' | 'complement' | 'adjustment' | 'devolution'; + +/** Consumer type */ +export type ProductInvoiceConsumerType = 'normal' | 'finalConsumer'; + +/** Buyer presence indicator */ +export type ProductInvoicePresenceType = 'none' | 'presence' | 'internet' | 'telephone' | 'delivery' | 'presenceOutOfStore' | 'othersNoPresente'; + +/** Process type for invoice emission */ +export type ProductInvoiceProcessType = 'ownSoftware' | 'fiscoSingle' | 'taxPayerSingle' | 'fiscoSoftware'; + +/** Tax regime code */ +export type ProductInvoiceTaxRegimeCode = 'national_Simple' | 'national_Simple_Brute' | 'normal_Regime'; + +/** Person type */ +export type ProductInvoicePersonType = 'undefined' | 'naturalPerson' | 'legalEntity'; + +/** Payment method */ +export type ProductInvoicePaymentMethod = 'cash' | 'cheque' | 'creditCard' | 'debitCard' | 'storeCredict' | 'foodVouchers' | 'mealVouchers' | 'giftVouchers' | 'fuelVouchers' | 'commercialDuplicate' | 'bankSlip' | 'unpaid' | 'others'; + +/** Card flag/brand */ +export type ProductInvoiceCardFlag = 'visa' | 'mastercard' | 'americanExpress' | 'sorocred' | 'dinnersClub' | 'elo' | 'hipercard' | 'aura' | 'cabal' | 'outros'; + +/** Integration payment type */ +export type ProductInvoiceIntegrationPaymentType = 'integrated' | 'notIntegrated'; + +// Nested types +// ---------------------------------------------------------------------------- + +/** City within an address */ +export interface ProductInvoiceCity { + code?: string; + name?: string; +} + +/** Address for issuer or buyer */ +export interface ProductInvoiceAddress { + phone?: string; + state?: string; + city?: ProductInvoiceCity; + district?: string; + additionalInformation?: string; + streetSuffix?: string; + street?: string; + number?: string; + postalCode?: string; + country?: string; +} + +/** Invoice issuer (emitente) */ +export interface ProductInvoiceIssuer { + federalTaxNumber?: number; + name?: string; + tradeName?: string; + address?: ProductInvoiceAddress; + stateTaxNumber?: string; + codeTaxRegime?: ProductInvoiceTaxRegimeCode; + cnae?: number; + im?: string; + iest?: number; + type?: ProductInvoicePersonType; +} + +/** Invoice buyer (destinatário) */ +export interface ProductInvoiceBuyer { + federalTaxNumber?: number; + name?: string; + address?: ProductInvoiceAddress; + stateTaxNumber?: string; + stateTaxNumberIndicator?: number; + email?: string; + type?: ProductInvoicePersonType; +} + +/** ICMS totals */ +export interface ProductInvoiceIcmsTotals { + baseTax?: number; + icmsAmount?: number; + icmsExemptAmount?: number; + stCalculationBasisAmount?: number; + stAmount?: number; + productAmount?: number; + freightAmount?: number; + insuranceAmount?: number; + discountAmount?: number; + iiAmount?: number; + ipiAmount?: number; + pisAmount?: number; + cofinsAmount?: number; + othersAmount?: number; + invoiceAmount?: number; + fcpufDestinationAmount?: number; + icmsufDestinationAmount?: number; + icmsufSenderAmount?: number; + federalTaxesAmount?: number; + fcpAmount?: number; + fcpstAmount?: number; + fcpstRetAmount?: number; + ipiDevolAmount?: number; +} + +/** ISSQN totals */ +export interface ProductInvoiceIssqnTotals { + totalServiceNotTaxedICMS?: number; + baseRateISS?: number; + totalISS?: number; + valueServicePIS?: number; + valueServiceCOFINS?: number; + provisionService?: string; + deductionReductionBC?: number; + valueOtherRetention?: number; + discountUnconditional?: number; + discountConditioning?: number; + totalRetentionISS?: number; + codeTaxRegime?: number; +} + +/** Invoice totals */ +export interface ProductInvoiceTotals { + icms?: ProductInvoiceIcmsTotals; + issqn?: ProductInvoiceIssqnTotals; +} + +/** ICMS tax on item */ +export interface ProductInvoiceItemIcms { + origin?: string; + cst?: string; + baseTaxModality?: string; + baseTax?: number; + baseTaxSTModality?: string; + baseTaxSTReduction?: number; + baseTaxSTAmount?: number; + baseTaxReduction?: number; + stRate?: number; + stAmount?: number; + stMarginAmount?: number; + csosn?: string; + rate?: number; + amount?: number; + snCreditRate?: string; + snCreditAmount?: string; + stMarginAddedAmount?: string; + stRetentionAmount?: string; + baseSTRetentionAmount?: string; + baseTaxOperationPercentual?: string; + ufst?: string; + amountSTUnfounded?: number; + amountSTReason?: string; + baseSNRetentionAmount?: string; + snRetentionAmount?: string; + amountOperation?: string; + percentualDeferment?: string; + baseDeferred?: string; + fcpRate?: number; + fcpAmount?: number; + fcpstRate?: number; + fcpstAmount?: number; + fcpstRetRate?: number; + fcpstRetAmount?: number; + bcfcpstAmount?: number; + finalConsumerRate?: number; + bcstRetIssuerAmount?: number; + stRetIssuerAmout?: number; + bcstBuyerAmount?: number; + stBuyerAmout?: number; + substituteAmount?: number; +} + +/** IPI tax on item */ +export interface ProductInvoiceItemIpi { + classification?: string; + producerCNPJ?: string; + stampCode?: string; + stampQuantity?: number; + classificationCode?: string; + cst?: string; + base?: string; + rate?: number; + unitQuantity?: number; + unitAmount?: number; + amount?: number; +} + +/** Import tax (II) on item */ +export interface ProductInvoiceItemII { + baseTax?: string; + customsExpenditureAmount?: string; + amount?: number; + iofAmount?: number; +} + +/** PIS tax on item */ +export interface ProductInvoiceItemPis { + cst?: string; + baseTax?: number; + rate?: number; + amount?: number; + baseTaxProductQuantity?: number; + productRate?: number; +} + +/** COFINS tax on item */ +export interface ProductInvoiceItemCofins { + cst?: string; + baseTax?: number; + rate?: number; + amount?: number; + baseTaxProductQuantity?: number; + productRate?: number; +} + +/** ICMS destination (interestadual) on item */ +export interface ProductInvoiceItemIcmsDestination { + vBCUFDest?: number; + pFCPUFDest?: number; + pICMSUFDest?: number; + pICMSInter?: number; + pICMSInterPart?: number; + vFCPUFDest?: number; + vICMSUFDest?: number; + vICMSUFRemet?: number; + vBCFCPUFDest?: number; +} + +/** Tax group on item */ +export interface ProductInvoiceItemTax { + totalTax?: number; + icms?: ProductInvoiceItemIcms; + ipi?: ProductInvoiceItemIpi; + ii?: ProductInvoiceItemII; + pis?: ProductInvoiceItemPis; + cofins?: ProductInvoiceItemCofins; + icmsDestination?: ProductInvoiceItemIcmsDestination; +} + +/** Medicine detail on item */ +export interface ProductInvoiceItemMedicine { + maximumPrice?: number; + anvisaCode?: string; + batchId?: string; + batchQuantity?: number; + manufacturedOn?: string; + expireOn?: string; +} + +/** Fuel CIDE information */ +export interface ProductInvoiceItemFuelCide { + bc?: number; + rate?: number; + cideAmount?: number; +} + +/** Fuel pump (encerrante) information */ +export interface ProductInvoiceItemFuelPump { + spoutNumber?: number; + number?: number; + tankNumber?: number; + beginningAmount?: number; + endAmount?: number; +} + +/** Fuel detail on item */ +export interface ProductInvoiceItemFuel { + codeANP?: string; + percentageNG?: number; + descriptionANP?: string; + percentageGLP?: number; + percentageNGn?: number; + percentageGNi?: number; + startingAmount?: number; + codif?: string; + amountTemp?: number; + stateBuyer?: string; + cide?: ProductInvoiceItemFuelCide; + pump?: ProductInvoiceItemFuelPump; +} + +/** Invoice item (product/service) */ +export interface ProductInvoiceItem { + code?: string; + codeGTIN?: string; + description?: string; + ncm?: string; + extipi?: string; + cfop?: number; + unit?: string; + quantity?: number; + unitAmount?: number; + totalAmount?: number; + eanTaxableCode?: string; + unitTax?: string; + quantityTax?: number; + taxUnitAmount?: number; + freightAmount?: number; + insuranceAmount?: number; + discountAmount?: number; + othersAmount?: number; + totalIndicator?: boolean; + cest?: string; + tax?: ProductInvoiceItemTax; + additionalInformation?: string; + numberOrderBuy?: string; + itemNumberOrderBuy?: number; + medicineDetail?: ProductInvoiceItemMedicine; + fuel?: ProductInvoiceItemFuel; +} + +/** Transport group (transportador) */ +export interface ProductInvoiceTransportGroup { + cityName?: string; + federalTaxNumber?: string; + cpf?: string; + name?: string; + stateTaxNumber?: string; + fullAddress?: string; + state?: string; + transportRetention?: string; +} + +/** Transport reboque (trailer) */ +export interface ProductInvoiceTransportReboque { + plate?: string; + uf?: string; + rntc?: string; + wagon?: string; + ferry?: string; +} + +/** Transport volume */ +export interface ProductInvoiceTransportVolume { + volumeQuantity?: number; + species?: string; + brand?: string; + volumeNumeration?: string; + netWeight?: number; + grossWeight?: number; +} + +/** Transport vehicle */ +export interface ProductInvoiceTransportVehicle { + plate?: string; + state?: string; + rntc?: string; +} + +/** Transport ICMS retention */ +export interface ProductInvoiceTransportRate { + serviceAmount?: number; + bcRetentionAmount?: number; + icmsRetentionRate?: number; + icmsRetentionAmount?: number; + cfop?: number; + cityGeneratorFactCode?: number; +} + +/** Transport information */ +export interface ProductInvoiceTransport { + freightModality?: number; + transportGroup?: ProductInvoiceTransportGroup; + reboque?: ProductInvoiceTransportReboque; + volume?: ProductInvoiceTransportVolume; + transportVehicle?: ProductInvoiceTransportVehicle; + sealNumber?: string; + transpRate?: ProductInvoiceTransportRate; +} + +/** Additional information */ +export interface ProductInvoiceAdditionalInfo { + fisco?: string; + taxpayer?: string; + xmlAuthorized?: number[]; + effort?: string; + order?: string; + contract?: string; + taxDocumentsReference?: ProductInvoiceTaxDocumentRef[]; + taxpayerComments?: ProductInvoiceTaxpayerComment[]; + referencedProcess?: ProductInvoiceReferencedProcess[]; +} + +/** Tax document reference */ +export interface ProductInvoiceTaxDocumentRef { + taxCouponInformation?: { + modelDocumentFiscal?: string; + orderECF?: string; + orderCountOperation?: number; + }; + documentInvoiceReference?: { + state?: number; + yearMonth?: string; + federalTaxNumber?: string; + model?: string; + series?: string; + number?: string; + }; + accessKey?: string; +} + +/** Taxpayer comment */ +export interface ProductInvoiceTaxpayerComment { + field?: string; + text?: string; +} + +/** Referenced process */ +export interface ProductInvoiceReferencedProcess { + identifierConcessory?: string; + identifierOrigin?: number; +} + +/** Protocol information */ +export interface ProductInvoiceProtocol { + id?: string; + environmentType?: ProductInvoiceEnvironmentType; + applicationVersion?: string; + accessKey?: string; + receiptOn?: string; + protocolNumber?: string; + validatorDigit?: string; + statusCode?: number; + description?: string; + signature?: string; +} + +/** Payment card details */ +export interface ProductInvoicePaymentCard { + federalTaxNumber?: string; + flag?: ProductInvoiceCardFlag; + authorization?: string; + integrationPaymentType?: ProductInvoiceIntegrationPaymentType; +} + +/** Payment detail entry */ +export interface ProductInvoicePaymentDetail { + method?: ProductInvoicePaymentMethod; + amount?: number; + card?: ProductInvoicePaymentCard; +} + +/** Payment group */ +export interface ProductInvoicePayment { + paymentDetail?: ProductInvoicePaymentDetail[]; + payBack?: number; +} + +/** Billing bill (fatura) */ +export interface ProductInvoiceBill { + number?: string; + originalAmount?: number; + discountAmount?: number; + netAmount?: number; +} + +/** Billing duplicate */ +export interface ProductInvoiceDuplicate { + duplicateNumber?: string; + expirationOn?: string; + amount?: number; +} + +/** Billing information (cobrança) */ +export interface ProductInvoiceBilling { + bill?: ProductInvoiceBill; + duplicates?: ProductInvoiceDuplicate[]; +} + +/** Full product invoice details returned by SEFAZ query */ +export interface ProductInvoiceDetails { + currentStatus?: ProductInvoiceStatus; + stateCode?: number; + checkCode?: number; + operationNature?: string; + paymentType?: ProductInvoicePaymentType; + codeModel?: number; + serie?: number; + number?: number; + issuedOn?: string; + operationOn?: string; + operationType?: ProductInvoiceOperationType; + destination?: ProductInvoiceDestination; + cityCode?: number; + printType?: ProductInvoicePrintType; + issueType?: ProductInvoiceIssueType; + checkCodeDigit?: number; + environmentType?: ProductInvoiceEnvironmentType; + purposeType?: ProductInvoicePurposeType; + consumerType?: ProductInvoiceConsumerType; + presenceType?: ProductInvoicePresenceType; + processType?: ProductInvoiceProcessType; + invoiceVersion?: string; + xmlVersion?: string; + contingencyOn?: string; + contingencyJustification?: string; + issuer?: ProductInvoiceIssuer; + buyer?: ProductInvoiceBuyer; + totals?: ProductInvoiceTotals; + transport?: ProductInvoiceTransport; + additionalInformation?: ProductInvoiceAdditionalInfo; + protocol?: ProductInvoiceProtocol; + items?: ProductInvoiceItem[]; + billing?: ProductInvoiceBilling; + payment?: ProductInvoicePayment[]; +} + +/** Fiscal event associated with a product invoice */ +export interface ProductInvoiceEvent { + stateCode?: number; + type?: number; + sequence?: number; + authorFederalTaxNumber?: string; + id?: string; + protocol?: number; + authorizedOn?: string; + description?: string; +} + +/** Response from listing fiscal events for a product invoice */ +export interface ProductInvoiceEventsResponse { + events?: ProductInvoiceEvent[]; + createdOn?: string; +} + +// ============================================================================ +// Consumer Invoice Query Types (CFe-SAT / Cupom Fiscal Eletrônico) +// ============================================================================ + +/** Status of a CFe-SAT consumer invoice (coupon) */ +export type CouponStatus = 'Unknown' | 'Authorized' | 'Canceled' | (string & {}); + +/** Person type for CFe-SAT entities */ +export type CouponPersonType = 'Undefined' | 'NaturalPerson' | 'LegalEntity' | (string & {}); + +/** Tax regime for CFe-SAT issuer */ +export type CouponTaxRegime = 'National_Simple' | 'National_Simple_Brute' | 'Normal_Regime' | (string & {}); + +/** Payment method for CFe-SAT coupon */ +export type CouponPaymentMethod = + | 'Cash' + | 'Cheque' + | 'CreditCard' + | 'DebitCard' + | 'StoreCredict' + | 'FoodVouchers' + | 'MealVouchers' + | 'GiftVouchers' + | 'FuelVouchers' + | 'CommercialDuplicate' + | 'BankSlip' + | 'BankDeposit' + | 'InstantPayment' + | 'WireTransfer' + | 'Cashback' + | 'Unpaid' + | 'Others' + | (string & {}); + +/** ISSQN tax incentive indicator */ +export type CouponIssqnTaxIncentive = 'Yes' | 'No' | (string & {}); + +/** City reference in CFe-SAT */ +export interface CouponCity { + code?: string; + name?: string; +} + +/** Address in CFe-SAT documents */ +export interface CouponAddress { + state?: string; + city?: CouponCity; + district?: string; + additionalInformation?: string; + streetSuffix?: string; + street?: string; + number?: string; + postalCode?: string; + country?: string; +} + +/** Issuer (emit) of a CFe-SAT coupon */ +export interface CouponIssuer { + federalTaxNumber?: number; + type?: CouponPersonType; + name?: string; + tradeName?: string; + address?: CouponAddress; + stateTaxNumber?: string; + taxRegime?: CouponTaxRegime; + municipalTaxNumber?: string; + iss?: string; + avarageIndicator?: boolean; +} + +/** Buyer (dest) of a CFe-SAT coupon */ +export interface CouponBuyer { + pretectedPersonalInformation?: string; + federalTaxNumber?: number; + name?: string; +} + +/** ICMS totals for a CFe-SAT coupon */ +export interface CouponIcmsTotal { + productAmount?: number; + discountAmount?: number; + othersAmount?: number; + icmsAmount?: number; + inputDiscountAmount?: number; + inputAdditionAmount?: number; + pisAmount?: number; + cofinsAmount?: number; + pisstAmount?: number; + cofinsstAmount?: number; +} + +/** ISSQN totals for a CFe-SAT coupon */ +export interface CouponIssqnTotal { + baseAmount?: number; + issAmount?: number; + pisAmount?: number; + cofinsAmount?: number; + pisstAmount?: number; + cofinsstAmount?: number; +} + +/** Totals for a CFe-SAT coupon */ +export interface CouponTotal { + icms?: CouponIcmsTotal; + issqn?: CouponIssqnTotal; + totalAmount?: number; + couponAmount?: number; +} + +/** Tax base resource (used by PIS/COFINS ST) */ +export interface CouponTaxBase { + baseTax?: number; + rate?: number; + amount?: number; + rateAmount?: number; + quantity?: number; +} + +/** ICMS tax data for a coupon item */ +export interface CouponIcmsTax { + origin?: string; + cst?: string; + csosn?: string; + amount?: number; + rate?: number; +} + +/** PIS tax data for a coupon item */ +export interface CouponPisTax { + cst?: string; + st?: CouponTaxBase; + baseTax?: number; + rate?: number; + amount?: number; + rateAmount?: number; + quantity?: number; +} + +/** COFINS tax data for a coupon item */ +export interface CouponCofinsTax { + cst?: string; + st?: CouponTaxBase; + baseTax?: number; + rate?: number; + amount?: number; + rateAmount?: number; + quantity?: number; +} + +/** ISSQN tax data for a coupon item */ +export interface CouponIssqnTax { + deductionsAmount?: number; + baseTax?: number; + rate?: number; + amount?: number; + federalServiceCode?: string; + cityServiceCode?: string; + cityCode?: number; + taxIncentive?: CouponIssqnTaxIncentive; + operationNature?: string; +} + +/** Tax breakdown for a coupon item */ +export interface CouponItemTax { + totalTax?: number; + icms?: CouponIcmsTax; + pis?: CouponPisTax; + cofins?: CouponCofinsTax; + issqn?: CouponIssqnTax; +} + +/** Fisco observation field */ +export interface CouponFiscoField { + key?: string; + value?: string; +} + +/** Referenced tax document */ +export interface CouponReferencedDocument { + accessKey?: string; + order?: number; +} + +/** Product item in a CFe-SAT coupon */ +export interface CouponItem { + description?: string; + quantity?: number; + unit?: string; + code?: string; + codeGTIN?: string; + ncm?: string; + cfop?: number; + cest?: string; + unitAmount?: number; + discountAmount?: number; + othersAmount?: number; + additionalInformation?: string; + itemNumberOrderBuy?: number; + netAmount?: number; + grossAmount?: number; + rule?: string; + apportionmentDiscountAmount?: number; + apportionmentAmount?: number; + fisco?: CouponFiscoField[]; + tax?: CouponItemTax; +} + +/** Payment detail in a CFe-SAT coupon */ +export interface CouponPaymentDetail { + method?: CouponPaymentMethod; + amount?: number; + card?: string; +} + +/** Payment group for a CFe-SAT coupon */ +export interface CouponPayment { + payBack?: number; + paymentDetails?: CouponPaymentDetail[]; +} + +/** Delivery information for a CFe-SAT coupon */ +export interface CouponDelivery { + address?: CouponAddress; +} + +/** Additional information for a CFe-SAT coupon */ +export interface CouponAdditionalInformation { + taxpayer?: string; + fisco?: CouponFiscoField[]; + referencedDocuments?: CouponReferencedDocument[]; +} + +/** CFe-SAT tax coupon (Cupom Fiscal Eletrônico) */ +export interface TaxCoupon { + currentStatus?: CouponStatus; + number?: number; + satSerie?: string; + softwareVersion?: string; + softwareFederalTaxNumber?: number; + accessKey?: string; + cashier?: number; + issuedOn?: string; + createdOn?: string; + xmlVersion?: string; + issuer?: CouponIssuer; + buyer?: CouponBuyer; + totals?: CouponTotal; + delivery?: CouponDelivery; + additionalInformation?: CouponAdditionalInformation; + items?: CouponItem[]; + payment?: CouponPayment; +} + +// ============================================================================ +// Legal Entity Lookup Types (consulta-cnpj) +// ============================================================================ + +/** Valid Brazilian state abbreviations (27 UFs + EX + NA) */ +export type BrazilianState = + | 'AC' | 'AL' | 'AM' | 'AP' | 'BA' | 'CE' | 'DF' | 'ES' | 'GO' + | 'MA' | 'MG' | 'MS' | 'MT' | 'PA' | 'PB' | 'PE' | 'PI' | 'PR' + | 'RJ' | 'RN' | 'RO' | 'RR' | 'RS' | 'SC' | 'SE' | 'SP' | 'TO' + | 'EX' | 'NA'; + +/** Options for basic info lookup */ +export interface LegalEntityBasicInfoOptions { + /** Whether to update the address from postal service data (default: true) */ + updateAddress?: boolean; + /** When updateAddress=false, whether to update only the city code from postal service data (default: false) */ + updateCityCode?: boolean; +} + +// --- Response Wrappers --- + +/** Response wrapper for CNPJ basic info lookup */ +export interface LegalEntityBasicInfoResponse { + /** Legal entity data */ + legalEntity?: LegalEntityBasicInfo; +} + +/** Response wrapper for state tax info lookup */ +export interface LegalEntityStateTaxResponse { + /** Legal entity state tax data */ + legalEntity?: LegalEntityStateTaxInfo; +} + +/** Response wrapper for state tax for invoice lookup */ +export interface LegalEntityStateTaxForInvoiceResponse { + /** Legal entity state tax data for invoice evaluation */ + legalEntity?: LegalEntityStateTaxForInvoiceInfo; +} + +// --- Core Entity Types --- + +/** Company size classification */ +export type LegalEntitySize = 'Unknown' | 'ME' | 'EPP' | 'DEMAIS'; + +/** Company registration status */ +export type LegalEntityStatus = 'Unknown' | 'Active' | 'Suspended' | 'Cancelled' | 'Unabled' | 'Null'; + +/** Organizational unit type */ +export type LegalEntityUnit = 'Headoffice' | 'Subsidiary'; + +/** Tax regime code */ +export type LegalEntityTaxRegime = 'Unknown' | 'SimplesNacional' | 'MEI' | 'Normal'; + +/** Legal nature classification */ +export type LegalEntityNatureCode = + | 'EmpresaPublica' | 'SociedadeEconomiaMista' | 'SociedadeAnonimaAberta' + | 'SociedadeAnonimaFechada' | 'SociedadeEmpresariaLimitada' + | 'SociedadeEmpresariaEmNomeColetivo' | 'SociedadeEmpresariaEmComanditaSimples' + | 'SociedadeEmpresariaEmComanditaporAcoes' | 'SociedadeemContaParticipacao' + | 'Empresario' | 'Cooperativa' | 'ConsorcioSociedades' | 'GrupoSociedades' + | 'EmpresaDomiciliadaExterior' | 'ClubeFundoInvestimento' + | 'SociedadeSimplesPura' | 'SociedadeSimplesLimitada' + | 'SociedadeSimplesEmNomeColetivo' | 'SociedadeSimplesEmComanditaSimples' + | 'EmpresaBinacional' | 'ConsorcioEmpregadores' | 'ConsorcioSimples' + | 'EireliNaturezaEmpresaria' | 'EireliNaturezaSimples' | 'ServicoNotarial' + | 'FundacaoPrivada' | 'ServicoSocialAutonomo' | 'CondominioEdilicio' + | 'ComissaoConciliacaoPrevia' | 'EntidadeMediacaoArbitragem' + | 'PartidoPolitico' | 'EntidadeSindical' + | 'EstabelecimentoBrasilFundacaoAssociacaoEstrangeiras' + | 'FundacaoAssociacaoDomiciliadaExterior' | 'OrganizacaoReligiosa' + | 'ComunidadeIndigena' | 'FundoPrivado' | 'AssociacaoPrivada' + | 'OutrasSemFimLucrativo' | 'Unknown'; + +/** State tax registration status */ +export type LegalEntityStateTaxStatus = 'Abled' | 'Unabled' | 'Cancelled' | 'Unknown'; + +/** Extended state tax registration status for invoice evaluation */ +export type LegalEntityStateTaxForInvoiceStatus = + | 'Abled' | 'Unabled' | 'Cancelled' + | 'UnabledTemp' | 'UnabledNotConfirmed' + | 'Unknown' | 'UnknownTemp' | 'UnknownNotConfirmed'; + +/** Fiscal document contributor status */ +export type LegalEntityFiscalDocumentStatus = 'Abled' | 'Unabled' | 'Unknown'; + +/** Economic activity type classification */ +export type LegalEntityActivityType = 'Main' | 'Secondary'; + +/** Phone source */ +export type LegalEntityPhoneSource = 'RFB'; + +// --- Nested Object Types --- + +/** City information */ +export interface LegalEntityCity { + /** City IBGE code */ + code?: string; + /** City name */ + name?: string; +} + +/** Address from Legal Entity API */ +export interface LegalEntityAddress { + /** State abbreviation (UF) */ + state?: string; + /** City information */ + city?: LegalEntityCity; + /** District / neighborhood */ + district?: string; + /** Additional address information */ + additionalInformation?: string; + /** Street suffix (type) */ + streetSuffix?: string; + /** Street name */ + street?: string; + /** Street number */ + number?: string; + /** Minimum number range */ + numberMin?: string; + /** Maximum number range */ + numberMax?: string; + /** Postal code (CEP) */ + postalCode?: string; + /** Country */ + country?: string; +} + +/** Phone number */ +export interface LegalEntityPhone { + /** Area code (DDD) */ + ddd?: string; + /** Phone number */ + number?: string; + /** Information source */ + source?: LegalEntityPhoneSource; +} + +/** Economic activity (CNAE) */ +export interface LegalEntityEconomicActivity { + /** Activity classification (Main or Secondary) */ + type?: LegalEntityActivityType; + /** CNAE code */ + code?: number; + /** CNAE description */ + description?: string; +} + +/** Legal nature */ +export interface LegalEntityNature { + /** Legal nature code */ + code?: string; + /** Legal nature description */ + description?: string; +} + +/** Partner qualification */ +export interface LegalEntityQualification { + /** Qualification code */ + code?: string; + /** Qualification description */ + description?: string; +} + +/** Company partner */ +export interface LegalEntityPartner { + /** Partner name */ + name?: string; + /** Partner qualification */ + qualification?: LegalEntityQualification; +} + +/** Fiscal document indicator (NFe/NFSe/CTe/NFCe) */ +export interface LegalEntityFiscalDocumentInfo { + /** Contributor status */ + status?: LegalEntityFiscalDocumentStatus; + /** Data source description */ + description?: string; +} + +/** State tax registration (Inscrição Estadual) */ +export interface LegalEntityStateTax { + /** Registration status */ + status?: LegalEntityStateTaxStatus; + /** State tax number (IE) */ + taxNumber?: string; + /** Status date */ + statusOn?: string; + /** Opening date */ + openedOn?: string; + /** Closing date */ + closedOn?: string; + /** Additional information */ + additionalInformation?: string; + /** State code */ + code?: BrazilianState; + /** Address */ + address?: LegalEntityAddress; + /** Economic activities (CNAE) */ + economicActivities?: LegalEntityEconomicActivity[]; + /** NFe indicator */ + nfe?: LegalEntityFiscalDocumentInfo; + /** NFSe indicator */ + nfse?: LegalEntityFiscalDocumentInfo; + /** CTe indicator */ + cte?: LegalEntityFiscalDocumentInfo; + /** NFCe indicator */ + nfce?: LegalEntityFiscalDocumentInfo; +} + +/** State tax registration for invoice evaluation (extended status) */ +export interface LegalEntityStateTaxForInvoice { + /** Registration status (extended enum) */ + status?: LegalEntityStateTaxForInvoiceStatus; + /** State tax number (IE) */ + taxNumber?: string; + /** Status date */ + statusOn?: string; + /** Opening date */ + openedOn?: string; + /** Closing date */ + closedOn?: string; + /** Additional information */ + additionalInformation?: string; + /** State code */ + code?: BrazilianState; + /** Address */ + address?: LegalEntityAddress; + /** Economic activities (CNAE) */ + economicActivities?: LegalEntityEconomicActivity[]; + /** NFe indicator */ + nfe?: LegalEntityFiscalDocumentInfo; + /** NFSe indicator */ + nfse?: LegalEntityFiscalDocumentInfo; + /** CTe indicator */ + cte?: LegalEntityFiscalDocumentInfo; + /** NFCe indicator */ + nfce?: LegalEntityFiscalDocumentInfo; +} + +// --- Main Entity Types --- + +/** Full company data from CNPJ basic info lookup */ +export interface LegalEntityBasicInfo { + /** Trade name (nome fantasia) */ + tradeName?: string; + /** Legal name (razão social) */ + name?: string; + /** Federal tax number (CNPJ) — numeric */ + federalTaxNumber?: number; + /** Company size classification */ + size?: LegalEntitySize; + /** Opening date */ + openedOn?: string; + /** Company address */ + address?: LegalEntityAddress; + /** Phone numbers */ + phones?: LegalEntityPhone[]; + /** Registration status date */ + statusOn?: string; + /** Registration status */ + status?: LegalEntityStatus; + /** Email address */ + email?: string; + /** Responsible federal entity (EFR) */ + responsableEntity?: string; + /** Special status */ + specialStatus?: string; + /** Special status date */ + specialStatusOn?: string; + /** Query date (when the data was fetched) */ + issuedOn?: string; + /** Status reason description */ + statusReason?: string; + /** Share capital in BRL */ + shareCapital?: number; + /** Economic activities (CNAE) */ + economicActivities?: LegalEntityEconomicActivity[]; + /** Legal nature */ + legalNature?: LegalEntityNature; + /** Partners and administrators */ + partners?: LegalEntityPartner[]; + /** Registration unit (city/office) */ + registrationUnit?: string; + /** Organizational unit (headquarters/subsidiary) */ + unit?: LegalEntityUnit; +} + +/** State tax information from state tax info lookup */ +export interface LegalEntityStateTaxInfo { + /** Trade name */ + tradeName?: string; + /** Legal name */ + name?: string; + /** Federal tax number (CNPJ) — numeric */ + federalTaxNumber?: number; + /** Query date */ + createdOn?: string; + /** Tax regime (CRT) */ + taxRegime?: LegalEntityTaxRegime; + /** Legal nature code */ + legalNature?: LegalEntityNatureCode; + /** Fiscal unit */ + fiscalUnit?: string; + /** Registration unit */ + createdUnit?: string; + /** Verification code */ + checkCode?: string; + /** State tax registrations (Inscrições Estaduais) */ + stateTaxes?: LegalEntityStateTax[]; +} + +/** State tax information for invoice evaluation */ +export interface LegalEntityStateTaxForInvoiceInfo { + /** Trade name */ + tradeName?: string; + /** Legal name */ + name?: string; + /** Federal tax number (CNPJ) — numeric */ + federalTaxNumber?: number; + /** Query date */ + createdOn?: string; + /** Tax regime (CRT) */ + taxRegime?: LegalEntityTaxRegime; + /** Legal nature code */ + legalNature?: LegalEntityNatureCode; + /** Fiscal unit */ + fiscalUnit?: string; + /** Registration unit */ + createdUnit?: string; + /** Verification code */ + checkCode?: string; + /** State tax registrations for invoice evaluation (extended status) */ + stateTaxes?: LegalEntityStateTaxForInvoice[]; +} + +// ============================================================================ +// Natural Person Lookup Types (consulta-cpf) +// ============================================================================ + +/** + * Known cadastral status values for CPF (situação cadastral na Receita Federal). + * The union includes a `(string & {})` fallback to allow unknown future values + * while still providing autocomplete for known statuses. + */ +export type NaturalPersonStatus = + | 'Regular' + | 'Suspensa' + | 'Cancelada' + | 'Titular Falecido' + | 'Pendente de Regularização' + | 'Nula' + | (string & {}); + +/** + * Response from the CPF cadastral status lookup endpoint. + * + * Returned by `GET /v1/naturalperson/status/{federalTaxNumber}/{birthDate}` + * on `naturalperson.api.nfe.io`. + */ +export interface NaturalPersonStatusResponse { + /** Full name of the person */ + name?: string; + /** CPF number (digits only) */ + federalTaxNumber: string; + /** Date of birth (ISO 8601 date-time string) */ + birthOn?: string; + /** Cadastral status at Receita Federal */ + status?: NaturalPersonStatus; + /** Timestamp of when the query was created (ISO 8601 date-time string) */ + createdOn?: string; +} + +// ============================================================================ +// Tax Calculation Types (calculo-impostos-v1) +// ============================================================================ + +// --- Enums --- + +/** + * Type of tax operation (incoming vs outgoing). + * + * - `'Outgoing'` — Saída (sale, shipment) + * - `'Incoming'` — Entrada (purchase, receipt) + */ +export type TaxOperationType = 'Outgoing' | 'Incoming'; + +/** + * Origin of the merchandise for ICMS purposes. + * + * Mirrors the SEFAZ origin codes (0-8). + */ +export type TaxOrigin = + | 'National' + | 'ForeignDirectImport' + | 'ForeignInternalMarket' + | 'NationalWith40To70Import' + | 'NationalPpb' + | 'NationalWithLess40Import' + | 'ForeignDirectImportWithoutNationalSimilar' + | 'ForeignInternalMarketWithoutNationalSimilar' + | 'NationalWithGreater70Import'; + +/** + * Tax regime used in the Tax Calculation Engine. + * + * **Note:** This differs from the service-invoice {@link TaxRegime} which uses + * Portuguese-language values. This enum uses the PascalCase values from the + * calculo-impostos API. + */ +export type TaxCalcTaxRegime = + | 'NationalSimple' + | 'RealProfit' + | 'PresumedProfit' + | 'NationalSimpleSublimitExceeded' + | 'IndividualMicroEnterprise' + | 'Exempt'; + +// --- Tax Component Interfaces --- + +/** + * ICMS tax component — covers ICMS, ICMS-ST, FCP, and related calculations. + * + * All numeric fields are represented as strings matching the API's format. + */ +export interface TaxIcms { + /** Origem da mercadoria */ + orig?: string; + /** Tributação do ICMS (CST) */ + cst?: string; + /** Código de Situação da Operação – Simples Nacional (CSOSN) */ + csosn?: string; + /** Modalidade de determinação da BC do ICMS */ + modBC?: string; + /** Valor da BC do ICMS */ + vBC?: string; + /** Percentual da Redução de BC */ + pRedBC?: string; + /** Código do benefício fiscal relacionado a redução de base */ + cBenefRBC?: string; + /** Alíquota do imposto */ + pICMS?: string; + /** Valor do ICMS */ + vICMS?: string; + /** Valor do ICMS da Operação */ + vICMSOp?: string; + /** Modalidade de determinação da BC do ICMS ST */ + modBCST?: string; + /** Valor da BC do ICMS ST */ + vBCST?: string; + /** Percentual da Redução de BC do ICMS ST */ + pRedBCST?: string; + /** Alíquota do imposto do ICMS ST */ + pICMSST?: string; + /** Valor do ICMS ST */ + vICMSST?: string; + /** Percentual da margem de valor Adicionado do ICMS ST */ + pMVAST?: string; + /** Alíquota suportada pelo Consumidor Final */ + pST?: string; + /** Valor da BC do ICMS ST retido */ + vBCSTRet?: string; + /** Valor do ICMS ST retido */ + vICMSSTRet?: string; + /** Valor da Base de Cálculo do FCP */ + vBCFCP?: string; + /** Percentual do ICMS relativo ao Fundo de Combate à Pobreza (FCP) */ + pFCP?: string; + /** Valor do Fundo de Combate à Pobreza (FCP) */ + vFCP?: string; + /** Valor da Base de Cálculo do FCP retido por Substituição Tributária */ + vBCFCPST?: string; + /** Percentual do FCP retido por Substituição Tributária */ + pFCPST?: string; + /** Valor do FCP retido por Substituição Tributária */ + vFCPST?: string; + /** Valor da Base de Cálculo do FCP retido anteriormente */ + vBCFCPSTRet?: string; + /** Percentual do FCP retido anteriormente por Substituição Tributária */ + pFCPSTRet?: string; + /** Valor do FCP retido por Substituição Tributária (retained) */ + vFCPSTRet?: string; + /** Valor da base de cálculo efetiva */ + vBCEfet?: string; + /** Percentual de redução da base de cálculo efetiva */ + pRedBCEfet?: string; + /** Alíquota do ICMS efetiva */ + pICMSEfet?: string; + /** Valor do ICMS efetivo */ + vICMSEfet?: string; + /** Percentual do diferimento */ + pDif?: string; + /** Valor do ICMS diferido */ + vICMSDif?: string; + /** Valor do ICMS próprio do Substituto */ + vICMSSubstituto?: string; + /** Alíquota aplicável de cálculo do crédito (Simples Nacional) */ + pCredSN?: string; + /** Valor crédito do ICMS (Simples Nacional, LC 123 art. 23) */ + vCredICMSSN?: string; + /** Percentual do diferimento do FCP */ + pFCPDif?: string; + /** Valor do FCP diferido */ + vFCPDif?: string; + /** Valor efetivo do FCP */ + vFCPEfet?: string; + /** Valor do ICMS desonerado */ + vICMSDeson?: string; + /** Motivo da desoneração do ICMS */ + motDesICMS?: string; + /** Valor do ICMS-ST desonerado */ + vICMSSTDeson?: string; + /** Motivo da desoneração do ICMS-ST */ + motDesICMSST?: string; + /** Indica se o valor do ICMS desonerado deduz do valor do item */ + indDeduzDeson?: string; +} + +/** + * ICMS interestadual (DIFAL / UF Destination) tax component. + */ +export interface TaxIcmsUfDest { + /** Valor da BC do ICMS na UF de destino */ + vBCUFDest?: string; + /** Valor da BC FCP na UF de destino */ + vBCFCPUFDest?: string; + /** Percentual do FCP na UF de destino */ + pFCPUFDest?: string; + /** Alíquota interna da UF de destino */ + pICMSUFDest?: string; + /** Alíquota interestadual das UF envolvidas */ + pICMSInter?: string; + /** Percentual provisório de partilha do ICMS Interestadual */ + pICMSInterPart?: string; + /** Valor do FCP na UF de destino */ + vFCPUFDest?: string; + /** Valor do ICMS Interestadual para a UF de destino */ + vICMSUFDest?: string; + /** Valor do ICMS Interestadual para a UF do remetente */ + vICMSUFRemet?: string; +} + +/** + * PIS tax component. + */ +export interface TaxPis { + /** Código de Situação Tributária do PIS */ + cst?: string; + /** Valor da Base de Cálculo do PIS */ + vBC?: string; + /** Alíquota do PIS (em percentual) */ + pPIS?: string; + /** Valor do PIS */ + vPIS?: string; + /** Quantidade Vendida */ + qBCProd?: string; + /** Alíquota do PIS (em reais) */ + vAliqProd?: string; +} + +/** + * COFINS tax component. + */ +export interface TaxCofins { + /** Código de Situação Tributária da COFINS */ + cst?: string; + /** Valor da Base de Cálculo do COFINS */ + vBC?: string; + /** Alíquota do COFINS (em percentual) */ + pCOFINS?: string; + /** Valor do COFINS */ + vCOFINS?: string; + /** Quantidade Vendida */ + qBCProd?: string; + /** Alíquota do COFINS (em reais) */ + vAliqProd?: string; +} + +/** + * IPI tax component. + */ +export interface TaxIpi { + /** Código de Enquadramento Legal do IPI */ + cEnq?: string; + /** Código da situação tributária do IPI */ + cst?: string; + /** Valor da BC do IPI */ + vBC?: string; + /** Alíquota do IPI */ + pIPI?: string; + /** Quantidade total na unidade padrão para tributação */ + qUnid?: string; + /** Valor por Unidade Tributável */ + vUnid?: string; + /** Valor do IPI */ + vIPI?: string; +} + +/** + * Import Tax (II) component. + */ +export interface TaxIi { + /** Valor BC do Imposto de Importação */ + vBC?: string; + /** Valor despesas aduaneiras */ + vDespAdu?: string; + /** Valor Imposto de Importação */ + vII?: string; + /** Valor Imposto sobre Operações Financeiras */ + vIOF?: string; + /** Valor dos encargos cambiais */ + vEncCamb?: string; + /** Alíquota do Simples Nacional aplicável */ + pCredSN?: string; + /** Valor crédito do ICMS (Simples Nacional) */ + vCredICMSSN?: string; + /** Ativação do cálculo do custo de aquisição (0=Inativo, 1=Ativo) */ + infCustoAquis?: string; +} + +// --- Request Interfaces --- + +/** + * Issuer data for the tax calculation request. + */ +export interface CalculateRequestIssuer { + /** Tax regime of the issuer */ + taxRegime: TaxCalcTaxRegime; + /** Default tax profile for the issuer */ + taxProfile?: string; + /** State of the issuer */ + state: BrazilianState; +} + +/** + * Recipient data for the tax calculation request. + */ +export interface CalculateRequestRecipient { + /** Tax regime of the recipient (optional) */ + taxRegime?: TaxCalcTaxRegime; + /** Default tax profile for the recipient */ + taxProfile?: string; + /** State of the recipient */ + state: BrazilianState; +} + +/** + * A single item (product) in the tax calculation request. + */ +export interface CalculateItemRequest { + /** Unique item identifier */ + id: string; + /** Internal code for operation nature determination (1–9999) */ + operationCode: number; + /** Acquisition purpose code */ + acquisitionPurpose?: string; + /** Issuer tax profile for this specific item */ + issuerTaxProfile?: string; + /** Recipient tax profile for this specific item */ + recipientTaxProfile?: string; + /** Product SKU */ + sku?: string; + /** NCM code (Nomenclatura Comum do Mercosul, up to 8 digits) */ + ncm?: string; + /** CEST code (Código Especificador da Substituição Tributária, 7 digits) */ + cest?: string; + /** Fiscal benefit code */ + benefit?: string; + /** EX TIPI code (1–3 chars) */ + exTipi?: string; + /** Origin of the merchandise */ + origin: TaxOrigin; + /** Global Trade Item Number */ + gtin?: string; + /** Taxable quantity */ + quantity: number; + /** Taxable unit amount */ + unitAmount: number; + /** Freight amount */ + freightAmount?: number; + /** Insurance amount */ + insuranceAmount?: number; + /** Discount amount */ + discountAmount?: number; + /** Other accessory expenses */ + othersAmount?: number; + /** ICMS input overrides (for import tax scenarios) */ + icms?: TaxIcms; + /** Import tax input overrides */ + ii?: TaxIi; +} + +/** + * Tax calculation request payload. + * + * Submit to `POST /tax-rules/{tenantId}/engine/calculate` to compute all + * applicable Brazilian taxes (ICMS, ICMS-ST, PIS, COFINS, IPI, II) for + * the given operation context and product items. + * + * @example + * ```typescript + * const request: CalculateRequest = { + * operationType: 'Outgoing', + * issuer: { state: 'SP', taxRegime: 'RealProfit' }, + * recipient: { state: 'RJ' }, + * items: [{ + * id: '1', + * operationCode: 121, + * origin: 'National', + * quantity: 10, + * unitAmount: 100.00, + * ncm: '61091000' + * }] + * }; + * ``` + */ +export interface CalculateRequest { + /** Product collection identifier */ + collectionId?: string; + /** Issuer (seller/shipper) fiscal data */ + issuer: CalculateRequestIssuer; + /** Recipient (buyer/receiver) fiscal data */ + recipient: CalculateRequestRecipient; + /** Type of operation */ + operationType: TaxOperationType; + /** List of products/items to calculate taxes for */ + items: CalculateItemRequest[]; + /** Whether this is a product registration request (vs invoice issuance) */ + isProductRegistration?: boolean; +} + +// --- Response Interfaces --- + +/** + * A single item in the tax calculation response with full tax breakdown. + */ +export interface CalculateItemResponse { + /** Item identifier (matches the request item id) */ + id?: string; + /** CFOP — Código Fiscal de Operações e Prestações */ + cfop?: number; + /** CEST code */ + cest?: string; + /** Fiscal benefit code */ + benefit?: string; + /** ICMS tax breakdown */ + icms?: TaxIcms; + /** ICMS interestadual (DIFAL / UF destination) breakdown */ + icmsUfDest?: TaxIcmsUfDest; + /** PIS tax breakdown */ + pis?: TaxPis; + /** COFINS tax breakdown */ + cofins?: TaxCofins; + /** IPI tax breakdown */ + ipi?: TaxIpi; + /** Import tax (II) breakdown */ + ii?: TaxIi; + /** Additional product information */ + additionalInformation?: string; + /** Timestamp of the last rule modification (ISO 8601) */ + lastModified?: string; + /** Registered product ID */ + productId?: string; +} + +/** + * Tax calculation response containing per-item tax breakdowns. + */ +export interface CalculateResponse { + /** Calculated items with full tax data */ + items?: CalculateItemResponse[]; +} + +// --- Tax Codes Types --- + +/** + * A single tax code entry (operation code, acquisition purpose, or tax profile). + */ +export interface TaxCode { + /** The code identifier */ + code?: string; + /** Human-readable description */ + description?: string; +} + +/** + * Paginated response for tax code listings. + */ +export interface TaxCodePaginatedResponse { + /** List of tax code entries */ + items?: TaxCode[]; + /** Current page number (1-based) */ + currentPage?: number; + /** Total number of pages */ + totalPages?: number; + /** Total count of entries */ + totalCount?: number; +} + +/** + * Options for listing tax codes (pagination). + * + * Uses the API's native pagination model (`pageIndex`/`pageCount`), + * which differs from the OData-style `$skip`/`$top` used by other resources. + */ +export interface TaxCodeListOptions { + /** Page index (1-based, default: 1) */ + pageIndex?: number; + /** Number of items per page (default: 50) */ + pageCount?: number; +} + +// ============================================================================ +// Product Invoice (NF-e Issuance) Types — nf-produto-v2 +// ============================================================================ + +// Enum types (string literal unions) +// ---------------------------------------------------------------------------- + +/** Environment type for NF-e product invoice operations */ +export type NfeEnvironmentType = 'None' | 'Production' | 'Test'; + +/** Status of a product invoice (NF-e) in the issuance lifecycle */ +export type NfeInvoiceStatus = + | 'None' + | 'Created' + | 'Processing' + | 'Issued' + | 'IssuedContingency' + | 'Cancelled' + | 'Disabled' + | 'IssueDenied' + | 'Error'; + +/** Brazilian state code (UF) */ +export type NfeStateCode = + | 'NA' | 'RO' | 'AC' | 'AM' | 'RR' | 'PA' | 'AP' | 'TO' + | 'MA' | 'PI' | 'CE' | 'RN' | 'PB' | 'PE' | 'AL' | 'SE' | 'BA' + | 'MG' | 'ES' | 'RJ' | 'SP' | 'PR' | 'SC' | 'RS' + | 'MS' | 'MT' | 'GO' | 'DF' | 'EX'; + +/** Operation type (incoming/outgoing) for NF-e */ +export type NfeOperationType = 'Outgoing' | 'Incoming'; + +/** Purpose of the NF-e invoice */ +export type NfePurposeType = 'None' | 'Normal' | 'Complement' | 'Adjustment' | 'Devolution'; + +/** Payment method for NF-e */ +export type NfePaymentMethod = + | 'Cash' | 'Cheque' | 'CreditCard' | 'DebitCard' + | 'StoreCredict' | 'FoodVouchers' | 'MealVouchers' | 'GiftVouchers' + | 'FuelVouchers' | 'BankBill' | 'BankDeposit' | 'InstantPayment' + | 'WireTransfer' | 'Cashback' | 'WithoutPayment' | 'Others'; + +/** Shipping modality for NF-e transport */ +export type NfeShippingModality = + | 'ByIssuer' | 'ByReceiver' | 'ByThirdParties' + | 'OwnBySender' | 'OwnByBuyer' | 'Free'; + +/** Consumer presence indicator for NF-e */ +export type NfeConsumerPresenceType = + | 'None' | 'Presence' | 'Internet' | 'Telephone' + | 'Delivery' | 'OthersNonPresenceOperation'; + +/** DANFE print format */ +export type NfePrintType = + | 'None' | 'NFeNormalPortrait' | 'NFeNormalLandscape' + | 'NFeSimplified' | 'DANFE_NFC_E' | 'DANFE_NFC_E_MSG_ELETRONICA'; + +/** Person type */ +export type NfePersonType = 'Undefined' | 'NaturalPerson' | 'LegalEntity' | 'Company' | 'Customer'; + +/** Destination of the operation */ +export type NfeDestination = + | 'None' | 'Internal_Operation' | 'Interstate_Operation' | 'International_Operation'; + +/** Consumer type indicator */ +export type NfeConsumerType = 'FinalConsumer' | 'Normal'; + +/** Payment type (cash or term) */ +export type NfePaymentType = 'InCash' | 'Term'; + +/** Receiver state tax indicator */ +export type NfeReceiverStateTaxIndicator = 'None' | 'TaxPayer' | 'Exempt' | 'NonTaxPayer'; + +/** Card flag/brand for payment */ +export type NfeFlagCard = + | 'None' | 'Visa' | 'Mastercard' | 'AmericanExpress' | 'Sorocred' + | 'DinersClub' | 'Elo' | 'Hipercard' | 'Aura' | 'Cabal' | 'Alelo' + | 'BanesCard' | 'CalCard' | 'Credz' | 'Discover' | 'GoodCard' + | 'GreenCard' | 'Hiper' | 'JCB' | 'Mais' | 'MaxVan' | 'Policard' + | 'RedeCompras' | 'Sodexo' | 'ValeCard' | 'Verocheque' | 'VR' + | 'Ticket' | 'Other'; + +/** Integration payment type */ +export type NfeIntegrationPaymentType = 'Integrated' | 'NotIntegrated'; + +/** Intermediation type */ +export type NfeIntermediationType = 'None' | 'ByOwn' | 'ImportOnBehalf' | 'ByOrder'; + +/** Tax regime */ +export type NfeTaxRegime = + | 'None' | 'LucroReal' | 'LucroPresumido' | 'SimplesNacional' + | 'SimplesNacionalExcessoSublimite' | 'MicroempreendedorIndividual' | 'Isento'; + +/** Special tax regime */ +export type NfeSpecialTaxRegime = + | 'Nenhum' | 'MicroempresaMunicipal' | 'Estimativa' + | 'SociedadeDeProfissionais' | 'Cooperativa' | 'MicroempreendedorIndividual' + | 'MicroempresarioEmpresaPequenoPorte' | 'Automatico'; + +/** State tax processing authorizer */ +export type NfeStateTaxProcessingAuthorizer = 'Normal' | 'EPEC'; + +/** Flow status for file operations */ +export type NfeFlowStatus = string; + +// Request/Response types — Product Invoices +// ---------------------------------------------------------------------------- + +/** Address in NF-e context */ +export interface NfeAddress { + /** Street name */ + street?: string; + /** Street number */ + number?: string; + /** Additional info (complement) */ + district?: string; + /** City */ + city?: NfeCity; + /** State code */ + state?: NfeStateCode; + /** Postal code (CEP) */ + postalCode?: string; + /** Country code */ + countryCode?: string; + /** Country name */ + country?: string; + /** Additional info */ + additionalInformation?: string; + [key: string]: unknown; +} + +/** City reference */ +export interface NfeCity { + /** IBGE city code */ + code?: string; + /** City name */ + name?: string; +} + +/** Buyer/recipient information for NF-e */ +export interface NfeProductInvoiceBuyer { + /** Buyer name */ + name?: string; + /** CNPJ or CPF (numeric) */ + federalTaxNumber?: number; + /** Email */ + email?: string; + /** Buyer address */ + address?: NfeAddress; + /** Person type */ + type?: NfePersonType; + /** State tax number (IE) */ + stateTaxNumber?: string; + /** State tax indicator */ + stateTaxNumberIndicator?: NfeReceiverStateTaxIndicator; + /** Trade name */ + tradeName?: string; + /** ISUF (SUFRAMA registration) */ + isuf?: string; + [key: string]: unknown; +} + +/** Card payment details */ +export interface NfeCardResource { + /** Card flag/brand */ + flagCard?: NfeFlagCard; + /** Integration type */ + integrationType?: NfeIntegrationPaymentType; + /** Authorization number */ + authorizationNumber?: string; + /** Card number */ + cardNumber?: string; + [key: string]: unknown; +} + +/** Payment detail entry */ +export interface NfePaymentDetail { + /** Payment method */ + method?: NfePaymentMethod; + /** Payment method description */ + methodDescription?: string; + /** Payment type (cash/term) */ + paymentType?: NfePaymentType; + /** Payment amount */ + amount?: number; + /** Card information */ + card?: NfeCardResource; + /** Payment date */ + paymentDate?: string; + /** CNPJ transacional do pagamento */ + federalTaxNumberPag?: string; + /** UF do CNPJ do pagamento */ + statePag?: string; + [key: string]: unknown; +} + +/** Payment group with details and change */ +export interface NfePaymentResource { + /** Payment details */ + paymentDetail?: NfePaymentDetail[]; + /** Change amount (troco) */ + payBack?: number; +} + +/** Billing information (cobrança) */ +export interface NfeBillingResource { + /** Invoice reference */ + invoice?: NfeBillingInvoice; + /** Duplicates (parcelas) */ + duplicates?: NfeDuplicateResource[]; +} + +/** Billing invoice reference */ +export interface NfeBillingInvoice { + /** Invoice number */ + number?: string; + /** Original amount */ + originalAmount?: number; + /** Discount amount */ + discountAmount?: number; + /** Net amount */ + netAmount?: number; + [key: string]: unknown; +} + +/** Billing duplicate (parcela) */ +export interface NfeDuplicateResource { + /** Duplicate number */ + number?: string; + /** Expiration date */ + expirationOn?: string; + /** Amount */ + amount?: number; +} + +/** ICMS tax information for an item */ +export interface NfeIcmsTaxResource { + /** Origin of goods */ + origin?: string; + /** CST (Código de Situação Tributária) */ + cst?: string; + /** CSOSN (Código de Situação da Operação – Simples Nacional) */ + csosn?: string; + /** Tax base amount */ + baseTax?: number; + /** Tax rate (%) */ + rate?: number; + /** Tax amount */ + amount?: number; + /** Modality of ICMS base calculation */ + modality?: number; + /** ICMS ST base amount */ + baseTaxST?: number; + /** ICMS ST rate */ + rateST?: number; + /** ICMS ST amount */ + amountST?: number; + [key: string]: unknown; +} + +/** IPI tax information */ +export interface NfeIpiTaxResource { + /** CST */ + cst?: string; + /** Tax base */ + baseTax?: number; + /** Rate */ + rate?: number; + /** Amount */ + amount?: number; + /** IPI enquadramento code */ + ipiCode?: string; + [key: string]: unknown; +} + +/** PIS tax information */ +export interface NfePisTaxResource { + /** CST */ + cst?: string; + /** Tax base */ + baseTax?: number; + /** Rate */ + rate?: number; + /** Amount */ + amount?: number; + /** Product quantity base */ + baseTaxProductQuantity?: number; + /** Product rate (in reais) */ + productRate?: number; +} + +/** COFINS tax information */ +export interface NfeCofinsTaxResource { + /** CST */ + cst?: string; + /** Tax base */ + baseTax?: number; + /** Rate */ + rate?: number; + /** Amount */ + amount?: number; + /** Product quantity base */ + baseTaxProductQuantity?: number; + /** Product rate (in reais) */ + productRate?: number; +} + +/** II (Import tax) information */ +export interface NfeIiTaxResource { + /** Tax base */ + baseTax?: number; + /** Custom expenses */ + customExpenses?: number; + /** IOF amount */ + iofAmount?: number; + /** II amount */ + amount?: number; +} + +/** ICMS UF Destination tax (partilha) */ +export interface NfeIcmsUfDestinationTaxResource { + /** Base tax amount */ + baseTax?: number; + /** FCP rate */ + fcpRate?: number; + /** Rate */ + rate?: number; + /** Interestadual rate */ + interestadualRate?: number; + /** Provisorio rate */ + provisorioRate?: number; + /** FCP amount */ + fcpAmount?: number; + /** Destination amount */ + destinationAmount?: number; + /** Origin amount */ + originAmount?: number; + [key: string]: unknown; +} + +/** Tax information for an invoice item */ +export interface NfeInvoiceItemTax { + /** Total approximate tax value */ + totalTax?: number; + /** ICMS tax */ + icms?: NfeIcmsTaxResource; + /** IPI tax */ + ipi?: NfeIpiTaxResource; + /** II (import) tax */ + ii?: NfeIiTaxResource; + /** PIS tax */ + pis?: NfePisTaxResource; + /** COFINS tax */ + cofins?: NfeCofinsTaxResource; + /** ICMS UF destination */ + icmsDestination?: NfeIcmsUfDestinationTaxResource; +} + +/** Tax determination resource for automatic tax calculation */ +export interface NfeTaxDeterminationResource { + /** Operation code for tax determination */ + operationCode?: number; + /** Issuer tax profile */ + issuerTaxProfile?: string; + /** Buyer tax profile */ + buyerTaxProfile?: string; + /** Origin */ + origin?: string; + /** Acquisition purpose */ + acquisitionPurpose?: string; +} + +/** Invoice item (product/service detail) */ +export interface NfeInvoiceItemResource { + /** Product/service code */ + code?: string; + /** GTIN barcode */ + codeGTIN?: string; + /** Product/service description */ + description?: string; + /** NCM code */ + ncm?: string; + /** NVE codes */ + nve?: string[]; + /** EXTIPI code */ + extipi?: string; + /** CFOP code */ + cfop?: number; + /** Commercial unit */ + unit?: string; + /** Commercial quantity */ + quantity?: number; + /** Unit amount */ + unitAmount?: number; + /** Total amount */ + totalAmount?: number; + /** Tax GTIN */ + codeTaxGTIN?: string; + /** Tax unit */ + unitTax?: string; + /** Tax quantity */ + quantityTax?: number; + /** Tax unit amount */ + taxUnitAmount?: number; + /** Freight amount */ + freightAmount?: number; + /** Insurance amount */ + insuranceAmount?: number; + /** Discount amount */ + discountAmount?: number; + /** Other expenses */ + othersAmount?: number; + /** Indicates if value enters total */ + totalIndicator?: boolean; + /** CEST code */ + cest?: string; + /** Tax details */ + tax?: NfeInvoiceItemTax; + /** Additional product information */ + additionalInformation?: string; + /** Purchase order number */ + numberOrderBuy?: string; + /** Purchase order item number */ + itemNumberOrderBuy?: number; + /** FCI number */ + importControlSheetNumber?: string; + /** Fuel details */ + fuelDetail?: Record; + /** Benefit code */ + benefit?: string; + /** Import declarations */ + importDeclarations?: Record[]; + /** Export details */ + exportDetails?: Record[]; + /** Tax determination */ + taxDetermination?: NfeTaxDeterminationResource; + [key: string]: unknown; +} + +/** Transport information for NF-e */ +export interface NfeTransportInformation { + /** Shipping modality */ + shippingModality?: NfeShippingModality; + /** Transport group (carrier info) */ + transportGroup?: NfeTransportGroupResource; + /** Volumes */ + volumes?: NfeVolumeResource[]; + [key: string]: unknown; +} + +/** Transport group/carrier resource */ +export interface NfeTransportGroupResource { + /** Carrier name */ + name?: string; + /** CNPJ or CPF */ + federalTaxNumber?: string; + /** State tax number (IE) */ + stateTaxNumber?: string; + /** Address (full) */ + address?: string; + /** City name */ + city?: string; + /** State code */ + state?: string; + /** Vehicle plate */ + vehiclePlate?: string; + /** Vehicle UF */ + vehicleUf?: string; + /** Vehicle RNTC */ + vehicleRntc?: string; + [key: string]: unknown; +} + +/** Volume resource for transport */ +export interface NfeVolumeResource { + /** Quantity */ + quantity?: number; + /** Species */ + species?: string; + /** Brand */ + brand?: string; + /** Numbering */ + numbering?: string; + /** Net weight */ + netWeight?: number; + /** Gross weight */ + grossWeight?: number; + /** Seal numbers */ + seals?: string[]; + [key: string]: unknown; +} + +/** Additional information for the invoice */ +export interface NfeAdditionalInformation { + /** Additional info for tax authority (infAdFisco) */ + taxAdministration?: string; + /** Complementary info for taxpayer (infCpl) */ + taxpayer?: string; + /** Referenced processes */ + referencedProcess?: Record[]; + [key: string]: unknown; +} + +/** Export hint and details */ +export interface NfeExportResource { + /** State that generated the invoice */ + exportState?: NfeStateCode; + /** Export location municipio */ + exportLocation?: string; + /** Export hint details */ + hint?: Record; + [key: string]: unknown; +} + +/** Issuer from request (issuer overrides) */ +export interface NfeIssuerFromRequest { + /** IE do Substituto Tributário (IEST) */ + stStateTaxNumber?: string; +} + +/** Transaction intermediate resource */ +export interface NfeIntermediateResource { + /** CNPJ of intermediary */ + federalTaxNumber?: number; + /** Identifier at intermediary */ + identifier?: string; +} + +/** Delivery information */ +export interface NfeDeliveryInformation { + /** Account ID */ + accountId?: string; + /** Entity ID */ + id?: string; + /** Name */ + name?: string; + /** CNPJ or CPF */ + federalTaxNumber?: number; + /** Email */ + email?: string; + /** Address */ + address?: NfeAddress; + /** Person type */ + type?: NfePersonType; + /** State tax number */ + stateTaxNumber?: string; + [key: string]: unknown; +} + +/** Withdrawal information */ +export interface NfeWithdrawalInformation { + /** Account ID */ + accountId?: string; + /** Entity ID */ + id?: string; + /** Name */ + name?: string; + /** CNPJ or CPF */ + federalTaxNumber?: number; + /** Email */ + email?: string; + /** Address */ + address?: NfeAddress; + /** Person type */ + type?: NfePersonType; + /** State tax number */ + stateTaxNumber?: string; + [key: string]: unknown; +} + +/** Totals (request — partial totals sent on issue) */ +export interface NfeTotals { + /** ICMS total */ + icms?: Record; + /** ISSQN total */ + issqn?: Record; + [key: string]: unknown; +} + +/** Total resource (response — full totals from API) */ +export interface NfeTotalResource { + /** ICMS total */ + icms?: Record; + /** ISSQN total */ + issqn?: Record; + [key: string]: unknown; +} + +/** Authorization details */ +export interface NfeAuthorizationResource { + /** Protocol number */ + protocol?: string; + /** Authorization date */ + sentOn?: string; + /** Authorization status */ + status?: string; + /** Access key (44 digits) */ + accessKey?: string; + /** Reason */ + reason?: string; + [key: string]: unknown; +} + +/** Contingency details */ +export interface NfeContingencyDetails { + /** Authorizer used */ + authorizer?: NfeStateTaxProcessingAuthorizer; + /** Start time */ + startedOn?: string; + /** Reason for contingency */ + reason?: string; +} + +/** Activity/event resource */ +export interface NfeActivityResource { + /** Event type */ + type?: string; + /** Event type description */ + typeDescription?: string; + /** Sequence number */ + sequence?: number; + /** Event creation date */ + createdOn?: string; + /** Event data */ + data?: Record; + [key: string]: unknown; +} + +/** Events base resource */ +export interface NfeInvoiceEventsBase { + /** List of events */ + events?: NfeActivityResource[]; + /** Whether more events exist */ + hasMore?: boolean; +} + +/** + * Data for issuing a product invoice (NF-e). + * Corresponds to `ProductInvoiceQueueIssueResource` in the OpenAPI spec. + */ +export interface NfeProductInvoiceIssueData { + /** Invoice ID (optional, auto-generated) */ + id?: string; + /** Invoice serie number */ + serie?: number; + /** Invoice number */ + number?: number; + /** Operation date/time (UTC ISO 8601) */ + operationOn?: string; + /** Operation nature description (natOp) */ + operationNature?: string; + /** Operation type */ + operationType?: NfeOperationType; + /** Destination */ + destination?: NfeDestination; + /** DANFE print type */ + printType?: NfePrintType; + /** Purpose type */ + purposeType?: NfePurposeType; + /** Consumer type */ + consumerType?: NfeConsumerType; + /** Consumer presence type */ + presenceType?: NfeConsumerPresenceType; + /** Contingency date/time */ + contingencyOn?: string; + /** Contingency justification */ + contingencyJustification?: string; + /** Buyer information */ + buyer?: NfeProductInvoiceBuyer; + /** Transport information */ + transport?: NfeTransportInformation; + /** Additional information */ + additionalInformation?: NfeAdditionalInformation; + /** Export information */ + export?: NfeExportResource; + /** Invoice items (products/services) */ + items?: NfeInvoiceItemResource[]; + /** Billing information */ + billing?: NfeBillingResource; + /** Issuer overrides */ + issuer?: NfeIssuerFromRequest; + /** Transaction intermediate */ + transactionIntermediate?: NfeIntermediateResource; + /** Delivery information */ + delivery?: NfeDeliveryInformation; + /** Withdrawal information */ + withdrawal?: NfeWithdrawalInformation; + /** Payment groups */ + payment?: NfePaymentResource[]; + /** Totals */ + totals?: NfeTotals; + [key: string]: unknown; +} + +/** Issuer resource (in responses) */ +export interface NfeIssuerResource { + /** Account ID */ + accountId?: string; + /** Issuer entity ID */ + id?: string; + /** Name or company name */ + name?: string; + /** CNPJ or CPF */ + federalTaxNumber?: number; + /** Email */ + email?: string; + /** Address */ + address?: NfeAddress; + /** Person type */ + type?: NfePersonType; + /** Trade name */ + tradeName?: string; + /** Opening date */ + openningDate?: string; + /** Tax regime */ + taxRegime?: NfeTaxRegime; + /** Special tax regime */ + specialTaxRegime?: NfeSpecialTaxRegime; + /** Regional tax number (IE) */ + regionalTaxNumber?: number; + /** Municipal tax number (IM) */ + municipalTaxNumber?: string; + /** State tax number for ST */ + stStateTaxNumber?: string; + [key: string]: unknown; +} + +/** + * Full product invoice (NF-e) response. + * Corresponds to `InvoiceResource` in the OpenAPI spec. + */ +export interface NfeProductInvoice { + /** Invoice ID */ + id?: string; + /** Serie number */ + serie?: number; + /** Invoice number */ + number?: number; + /** Invoice status */ + status?: NfeInvoiceStatus; + /** Authorization details */ + authorization?: NfeAuthorizationResource; + /** Contingency details */ + contingencyDetails?: NfeContingencyDetails; + /** Operation nature */ + operationNature?: string; + /** Creation date */ + createdOn?: string; + /** Modification date */ + modifiedOn?: string; + /** Operation date */ + operationOn?: string; + /** Operation type */ + operationType?: NfeOperationType; + /** Environment type */ + environmentType?: NfeEnvironmentType; + /** Purpose type */ + purposeType?: NfePurposeType; + /** Issuer */ + issuer?: NfeIssuerResource; + /** Buyer */ + buyer?: NfeProductInvoiceBuyer; + /** Totals */ + totals?: NfeTotalResource; + /** Transport information */ + transport?: NfeTransportInformation; + /** Additional information */ + additionalInformation?: NfeAdditionalInformation; + /** Export information */ + export?: NfeExportResource; + /** Billing */ + billing?: NfeBillingResource; + /** Payment groups */ + payment?: NfePaymentResource[]; + /** Transaction intermediate */ + transactionIntermediate?: NfeIntermediateResource; + /** Delivery information */ + delivery?: NfeDeliveryInformation; + /** Withdrawal information */ + withdrawal?: NfeWithdrawalInformation; + /** Last events */ + lastEvents?: NfeInvoiceEventsBase; + [key: string]: unknown; +} + +/** Product invoice without events (used in list responses) */ +export interface NfeProductInvoiceWithoutEvents { + /** Invoice ID */ + id?: string; + /** Serie number */ + serie?: number; + /** Invoice number */ + number?: number; + /** Invoice status */ + status?: NfeInvoiceStatus; + /** Authorization details */ + authorization?: NfeAuthorizationResource; + /** Contingency details */ + contingencyDetails?: NfeContingencyDetails; + /** Operation nature */ + operationNature?: string; + /** Creation date */ + createdOn?: string; + /** Modification date */ + modifiedOn?: string; + /** Operation date */ + operationOn?: string; + /** Operation type */ + operationType?: NfeOperationType; + /** Environment type */ + environmentType?: NfeEnvironmentType; + /** Purpose type */ + purposeType?: NfePurposeType; + /** Issuer */ + issuer?: NfeIssuerResource; + /** Buyer */ + buyer?: NfeProductInvoiceBuyer; + /** Totals */ + totals?: NfeTotalResource; + /** Transport information */ + transport?: NfeTransportInformation; + /** Additional information */ + additionalInformation?: NfeAdditionalInformation; + /** Export information */ + export?: NfeExportResource; + /** Billing */ + billing?: NfeBillingResource; + /** Payment groups */ + payment?: NfePaymentResource[]; + /** Transaction intermediate */ + transactionIntermediate?: NfeIntermediateResource; + /** Delivery information */ + delivery?: NfeDeliveryInformation; + /** Withdrawal information */ + withdrawal?: NfeWithdrawalInformation; + [key: string]: unknown; +} + +/** Options for listing product invoices (cursor-based pagination) */ +export interface NfeProductInvoiceListOptions { + /** Environment (required) */ + environment: NfeEnvironmentType; + /** Cursor: start after this ID */ + startingAfter?: string; + /** Cursor: end before this ID */ + endingBefore?: string; + /** Number of results per page (default: 10) */ + limit?: number; + /** ElasticSearch query string */ + q?: string; +} + +/** Paginated list of product invoices */ +export interface NfeProductInvoiceListResponse { + /** List of invoices (without events) */ + productInvoices?: NfeProductInvoiceWithoutEvents[]; + /** Whether more results exist */ + hasMore?: boolean; +} + +/** Paginated list of invoice items */ +export interface NfeInvoiceItemsResponse { + /** Account ID */ + accountId?: string; + /** Company ID */ + companyId?: string; + /** Invoice ID */ + id?: string; + /** Invoice items */ + items?: NfeInvoiceItemResource[]; + /** Whether more items exist */ + hasMore?: boolean; +} + +/** Paginated list of invoice events */ +export interface NfeProductInvoiceEventsResponse { + /** Invoice ID */ + id?: string; + /** Account ID */ + accountId?: string; + /** Company ID */ + companyId?: string; + /** List of events */ + events?: NfeActivityResource[]; + /** Whether more events exist */ + hasMore?: boolean; +} + +/** Options for listing items/events (cursor pagination) */ +export interface NfeProductInvoiceSubListOptions { + /** Number of results per page (default: 10) */ + limit?: number; + /** Cursor: start after (default: 0) */ + startingAfter?: number | string; +} + +/** File resource (PDF/XML download response) */ +export interface NfeFileResource { + /** Absolute URI to the file */ + uri?: string; +} + +/** Request cancellation response */ +export interface NfeRequestCancellationResource { + /** Account ID */ + accountId?: string; + /** Company ID */ + companyId?: string; + /** Product invoice ID */ + productInvoiceId?: string; + /** Reason for cancellation */ + reason?: string; +} + +/** Disablement request data */ +export interface NfeDisablementData { + /** Environment */ + environment: NfeEnvironmentType; + /** Serie number */ + serie: number; + /** State code */ + state: NfeStateCode; + /** Beginning invoice number */ + beginNumber: number; + /** Last invoice number (same as beginNumber for a single number) */ + lastNumber: number; + /** Reason for disablement */ + reason?: string; +} + +/** Disablement response */ +export interface NfeDisablementResource { + /** Environment */ + environment?: NfeEnvironmentType; + /** Serie */ + serie?: number; + /** State code */ + state?: NfeStateCode; + /** Beginning number */ + beginNumber?: number; + /** Last number */ + lastNumber?: number; + /** Reason */ + reason?: string; +} + +// ============================================================================ +// State Tax (Inscrição Estadual) Types — nf-produto-v2 +// ============================================================================ + +/** State tax type (emission type) */ +export type NfeStateTaxType = 'default' | 'nFe' | 'nFCe'; + +/** State tax environment type */ +export type NfeStateTaxEnvironmentType = 'none' | 'production' | 'test'; + +/** State tax status */ +export type NfeStateTaxStatus = 'inactive' | 'none' | 'active'; + +/** State tax state code (lowercase as in API) */ +export type NfeStateTaxStateCode = + | 'rO' | 'aC' | 'aM' | 'rR' | 'pA' | 'aP' | 'tO' + | 'mA' | 'pI' | 'cE' | 'rN' | 'pB' | 'pE' | 'aL' | 'sE' | 'bA' + | 'mG' | 'eS' | 'rJ' | 'sP' | 'pR' | 'sC' | 'rS' + | 'mS' | 'mT' | 'gO' | 'dF' | 'eX' | 'nA'; + +/** State tax special tax regime (lowercase as in API) */ +export type NfeStateTaxSpecialTaxRegime = + | 'automatico' | 'nenhum' | 'microempresaMunicipal' | 'estimativa' + | 'sociedadeDeProfissionais' | 'cooperativa' | 'microempreendedorIndividual' + | 'microempresarioEmpresaPequenoPorte'; + +/** Security credential for NFCe */ +export interface NfeSecurityCredential { + /** Credential ID */ + id?: number; + /** Security code */ + code?: string; +} + +/** Full state tax record (response) */ +export interface NfeStateTax { + /** State tax ID */ + id?: string; + /** Company ID */ + companyId?: string; + /** Account ID */ + accountId?: string; + /** State code */ + code?: NfeStateTaxStateCode; + /** Environment type */ + environmentType?: NfeStateTaxEnvironmentType; + /** State tax number (IE) */ + taxNumber?: string; + /** Serie for emission */ + serie?: number; + /** Number for emission */ + number?: number; + /** Status */ + status?: NfeStateTaxStatus; + /** Special tax regime */ + specialTaxRegime?: NfeStateTaxSpecialTaxRegime; + /** Security credential (for NFCe) */ + securityCredential?: NfeSecurityCredential; + /** Emission type */ + type?: NfeStateTaxType; + /** All series for this state tax */ + series?: number[]; + /** Batch ID */ + batchId?: number; + /** Creation date */ + createdOn?: string; + /** Modification date */ + modifiedOn?: string; +} + +/** Data for creating a state tax registration */ +export interface NfeStateTaxCreateData { + /** State tax number (IE) — required */ + taxNumber: string; + /** Serie for emission — required */ + serie: number; + /** Number for emission — required */ + number: number; + /** State code */ + code?: NfeStateTaxStateCode; + /** Environment type */ + environmentType?: NfeStateTaxEnvironmentType; + /** Special tax regime */ + specialTaxRegime?: NfeStateTaxSpecialTaxRegime; + /** Security credential (for NFCe) */ + securityCredential?: NfeSecurityCredential; + /** Emission type */ + type?: NfeStateTaxType; +} + +/** Data for updating a state tax registration */ +export interface NfeStateTaxUpdateData { + /** State tax number (IE) */ + taxNumber?: string; + /** Serie for emission */ + serie?: number; + /** Number for emission */ + number?: number; + /** State code */ + code?: NfeStateTaxStateCode; + /** Environment type */ + environmentType?: NfeStateTaxEnvironmentType; + /** Special tax regime */ + specialTaxRegime?: NfeStateTaxSpecialTaxRegime; + /** Security credential (for NFCe) */ + securityCredential?: NfeSecurityCredential; + /** Emission type */ + type?: NfeStateTaxType; +} + +/** Paginated list of state tax registrations */ +export interface NfeStateTaxListResponse { + /** List of state taxes */ + stateTaxes?: NfeStateTax[]; +} + +/** Options for listing state taxes (cursor pagination) */ +export interface NfeStateTaxListOptions { + /** Cursor: start after this ID */ + startingAfter?: string; + /** Cursor: end before this ID */ + endingBefore?: string; + /** Number of results per page (default: 10) */ + limit?: number; +} diff --git a/src/generated/calculo-impostos-v1.ts b/src/generated/calculo-impostos-v1.ts index a6f46a5..1ea865f 100644 --- a/src/generated/calculo-impostos-v1.ts +++ b/src/generated/calculo-impostos-v1.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.163Z + * Last generated: 2026-02-15T17:59:25.411Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-cte-v2.ts b/src/generated/consulta-cte-v2.ts index 301c89d..574c5f1 100644 --- a/src/generated/consulta-cte-v2.ts +++ b/src/generated/consulta-cte-v2.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.183Z + * Last generated: 2026-02-15T17:59:25.439Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-nfe-distribuicao-v1.ts b/src/generated/consulta-nfe-distribuicao-v1.ts index 31e8515..977e2e9 100644 --- a/src/generated/consulta-nfe-distribuicao-v1.ts +++ b/src/generated/consulta-nfe-distribuicao-v1.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.215Z + * Last generated: 2026-02-15T17:59:25.479Z * Generator: openapi-typescript */ diff --git a/src/generated/index.ts b/src/generated/index.ts index efdb072..9b320e8 100644 --- a/src/generated/index.ts +++ b/src/generated/index.ts @@ -5,7 +5,7 @@ * Types are namespaced by spec to avoid conflicts. * * @generated - * Last updated: 2026-01-31T05:41:36.458Z + * Last updated: 2026-02-15T17:59:25.794Z */ // ============================================================================ diff --git a/src/generated/nf-consumidor-v2.ts b/src/generated/nf-consumidor-v2.ts index 0717bb9..db67918 100644 --- a/src/generated/nf-consumidor-v2.ts +++ b/src/generated/nf-consumidor-v2.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.312Z + * Last generated: 2026-02-15T17:59:25.613Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-produto-v2.ts b/src/generated/nf-produto-v2.ts index c59764e..e2ed289 100644 --- a/src/generated/nf-produto-v2.ts +++ b/src/generated/nf-produto-v2.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.391Z + * Last generated: 2026-02-15T17:59:25.708Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-servico-v1.ts b/src/generated/nf-servico-v1.ts index 1351271..3a555d7 100644 --- a/src/generated/nf-servico-v1.ts +++ b/src/generated/nf-servico-v1.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.449Z + * Last generated: 2026-02-15T17:59:25.778Z * Generator: openapi-typescript */ diff --git a/src/generated/nfeio.ts b/src/generated/nfeio.ts index aeefafa..93343d3 100644 --- a/src/generated/nfeio.ts +++ b/src/generated/nfeio.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-01-31T05:41:36.457Z + * Last generated: 2026-02-15T17:59:25.793Z * Generator: openapi-typescript */ diff --git a/src/index.ts b/src/index.ts index 31ba08f..9a8a05a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,7 +47,7 @@ * @see {@link NfeClient} - Main client class for NFE.io API * @see {@link createNfeClient} - Factory function for creating client instances */ -export { NfeClient, createNfeClient, VERSION, SUPPORTED_NODE_VERSIONS } from './core/client.js'; +export { NfeClient, createNfeClient, VERSION, SUPPORTED_NODE_VERSIONS, CTE_API_BASE_URL, LEGAL_ENTITY_API_BASE_URL, NATURAL_PERSON_API_BASE_URL } from './core/client.js'; /** * TypeScript type definitions for NFE.io API entities and configurations @@ -81,6 +81,227 @@ export type { AddressLookupResponse, AddressSearchOptions, + // CT-e (Transportation Invoice) types + TransportationInvoiceInboundSettings, + TransportationInvoiceMetadata, + EnableTransportationInvoiceOptions, + TransportationInvoiceEntityStatus, + TransportationInvoiceMetadataType, + + // Inbound NF-e Distribution types + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings, + EnableInboundOptions, + ManifestEventType, + InboundCompany, + InboundIssuer, + InboundBuyer, + InboundTransportation, + InboundLinks, + InboundProductInvoice, + AutomaticManifesting, + + // Product Invoice Query types (consulta-nf) + ProductInvoiceStatus, + ProductInvoicePaymentType, + ProductInvoiceOperationType, + ProductInvoiceDestination, + ProductInvoicePrintType, + ProductInvoiceIssueType, + ProductInvoiceEnvironmentType, + ProductInvoicePurposeType, + ProductInvoiceConsumerType, + ProductInvoicePresenceType, + ProductInvoiceProcessType, + ProductInvoiceTaxRegimeCode, + ProductInvoicePersonType, + ProductInvoicePaymentMethod, + ProductInvoiceCardFlag, + ProductInvoiceIntegrationPaymentType, + ProductInvoiceCity, + ProductInvoiceAddress, + ProductInvoiceIssuer, + ProductInvoiceBuyer, + ProductInvoiceIcmsTotals, + ProductInvoiceIssqnTotals, + ProductInvoiceTotals, + ProductInvoiceItemIcms, + ProductInvoiceItemTax, + ProductInvoiceItem, + ProductInvoiceTransport, + ProductInvoicePayment, + ProductInvoiceProtocol, + ProductInvoiceAdditionalInfo, + ProductInvoiceBilling, + ProductInvoiceDetails, + ProductInvoiceEvent, + ProductInvoiceEventsResponse, + + // Consumer Invoice Query types (CFe-SAT / consulta-nf-consumidor) + CouponStatus, + CouponPersonType, + CouponTaxRegime, + CouponPaymentMethod, + CouponIssqnTaxIncentive, + CouponCity, + CouponAddress, + CouponIssuer, + CouponBuyer, + CouponIcmsTotal, + CouponIssqnTotal, + CouponTotal, + CouponTaxBase, + CouponIcmsTax, + CouponPisTax, + CouponCofinsTax, + CouponIssqnTax, + CouponItemTax, + CouponFiscoField, + CouponReferencedDocument, + CouponItem, + CouponPaymentDetail, + CouponPayment, + CouponDelivery, + CouponAdditionalInformation, + TaxCoupon, + + // Legal Entity Lookup types (consulta-cnpj) + BrazilianState, + LegalEntityBasicInfoOptions, + LegalEntityBasicInfoResponse, + LegalEntityStateTaxResponse, + LegalEntityStateTaxForInvoiceResponse, + LegalEntitySize, + LegalEntityStatus, + LegalEntityUnit, + LegalEntityTaxRegime, + LegalEntityNatureCode, + LegalEntityStateTaxStatus, + LegalEntityStateTaxForInvoiceStatus, + LegalEntityFiscalDocumentStatus, + LegalEntityActivityType, + LegalEntityPhoneSource, + LegalEntityCity, + LegalEntityAddress, + LegalEntityPhone, + LegalEntityEconomicActivity, + LegalEntityNature, + LegalEntityQualification, + LegalEntityPartner, + LegalEntityFiscalDocumentInfo, + LegalEntityStateTax, + LegalEntityStateTaxForInvoice, + LegalEntityBasicInfo, + LegalEntityStateTaxInfo, + LegalEntityStateTaxForInvoiceInfo, + + // Natural Person Lookup types (consulta-cpf) + NaturalPersonStatus, + NaturalPersonStatusResponse, + + // Tax Calculation types (calculo-impostos) + TaxOperationType, + TaxOrigin, + TaxCalcTaxRegime, + TaxIcms, + TaxIcmsUfDest, + TaxPis, + TaxCofins, + TaxIpi, + TaxIi, + CalculateRequestIssuer, + CalculateRequestRecipient, + CalculateItemRequest, + CalculateRequest, + CalculateItemResponse, + CalculateResponse, + TaxCode, + TaxCodePaginatedResponse, + TaxCodeListOptions, + + // NF-e Product Invoice types (nf-produto-v2) + NfeEnvironmentType, + NfeInvoiceStatus, + NfeStateCode, + NfeOperationType, + NfePurposeType, + NfePaymentMethod, + NfeShippingModality, + NfeConsumerPresenceType, + NfePrintType, + NfePersonType, + NfeDestination, + NfeConsumerType, + NfePaymentType, + NfeReceiverStateTaxIndicator, + NfeFlagCard, + NfeIntegrationPaymentType, + NfeIntermediationType, + NfeTaxRegime, + NfeSpecialTaxRegime, + NfeStateTaxProcessingAuthorizer, + NfeFlowStatus, + NfeAddress, + NfeCity, + NfeProductInvoiceBuyer, + NfeCardResource, + NfePaymentDetail, + NfePaymentResource, + NfeBillingResource, + NfeBillingInvoice, + NfeDuplicateResource, + NfeIcmsTaxResource, + NfeIpiTaxResource, + NfePisTaxResource, + NfeCofinsTaxResource, + NfeIiTaxResource, + NfeIcmsUfDestinationTaxResource, + NfeInvoiceItemTax, + NfeTaxDeterminationResource, + NfeInvoiceItemResource, + NfeTransportInformation, + NfeTransportGroupResource, + NfeVolumeResource, + NfeAdditionalInformation, + NfeExportResource, + NfeIssuerFromRequest, + NfeIntermediateResource, + NfeDeliveryInformation, + NfeWithdrawalInformation, + NfeTotals, + NfeTotalResource, + NfeAuthorizationResource, + NfeContingencyDetails, + NfeActivityResource, + NfeInvoiceEventsBase, + NfeProductInvoiceIssueData, + NfeIssuerResource, + NfeProductInvoice, + NfeProductInvoiceWithoutEvents, + NfeProductInvoiceListOptions, + NfeProductInvoiceListResponse, + NfeInvoiceItemsResponse, + NfeProductInvoiceEventsResponse, + NfeProductInvoiceSubListOptions, + NfeFileResource, + NfeRequestCancellationResource, + NfeDisablementData, + NfeDisablementResource, + + // State Tax (Inscrição Estadual) types + NfeStateTaxType, + NfeStateTaxEnvironmentType, + NfeStateTaxStatus, + NfeStateTaxStateCode, + NfeStateTaxSpecialTaxRegime, + NfeStateTax, + NfeStateTaxCreateData, + NfeStateTaxUpdateData, + NfeStateTaxListResponse, + NfeStateTaxListOptions, + NfeSecurityCredential, + // Common types EntityType, TaxRegime, @@ -175,6 +396,31 @@ export { */ export { CertificateValidator } from './core/utils/certificate-validator.js'; +// ============================================================================ +// Resource Classes (for advanced usage) +// ============================================================================ + +/** + * Transportation Invoices (CT-e) Resource + * + * @see {@link TransportationInvoicesResource} - CT-e operations via Distribuição DFe + * + * @example + * ```typescript + * import { TransportationInvoicesResource } from '@nfe-io/sdk'; + * + * // For advanced usage when extending the SDK + * class CustomCteResource extends TransportationInvoicesResource { + * // Add custom methods + * } + * ``` + */ +export { TransportationInvoicesResource } from './core/resources/transportation-invoices.js'; +export { TaxCalculationResource, createTaxCalculationResource } from './core/resources/tax-calculation.js'; +export { TaxCodesResource, createTaxCodesResource } from './core/resources/tax-codes.js'; +export { ProductInvoicesResource } from './core/resources/product-invoices.js'; +export { StateTaxesResource } from './core/resources/state-taxes.js'; + // ============================================================================ // Default Export (maintains v2 compatibility) // ============================================================================ diff --git a/tests/integration/addresses.integration.test.ts b/tests/integration/addresses.integration.test.ts index 1fcff4e..864540a 100644 --- a/tests/integration/addresses.integration.test.ts +++ b/tests/integration/addresses.integration.test.ts @@ -5,7 +5,7 @@ * Skip these tests in CI/CD unless API key is available. * * To run these tests: - * 1. Set NFE_ADDRESS_API_KEY or NFE_API_KEY environment variable + * 1. Set NFE_DATA_API_KEY or NFE_API_KEY environment variable * 2. Run: npm run test:integration */ @@ -24,8 +24,8 @@ describe.skipIf(!shouldRun)('AddressesResource Integration', () => { beforeAll(() => { client = new NfeClient({ apiKey: process.env.NFE_API_KEY, - addressApiKey: - process.env.NFE_ADDRESS_API_KEY || + dataApiKey: + process.env.NFE_DATA_API_KEY || process.env.INTEGRATION_TEST_API_KEY, environment: 'production', }); @@ -124,21 +124,21 @@ describe.skipIf(!shouldRun)('AddressesResource Integration', () => { * Tests for multi-API key configuration in integration */ describe.skipIf(!shouldRun)('Multi-API Key Integration', () => { - it('should create client with only addressApiKey', () => { + it('should create client with only dataApiKey', () => { const client = new NfeClient({ - addressApiKey: process.env.NFE_ADDRESS_API_KEY || process.env.NFE_API_KEY, + dataApiKey: process.env.NFE_DATA_API_KEY || process.env.NFE_API_KEY, }); // Should be able to access addresses expect(() => client.addresses).not.toThrow(); }); - it('should make address API call with separate addressApiKey', async () => { + it('should make address API call with separate dataApiKey', async () => { const client = new NfeClient({ - addressApiKey: process.env.NFE_ADDRESS_API_KEY || process.env.NFE_API_KEY, + dataApiKey: process.env.NFE_DATA_API_KEY || process.env.NFE_API_KEY, }); - // This should work because we have addressApiKey + // This should work because we have dataApiKey const result = await client.addresses.lookupByPostalCode('01310100'); expect(result).toBeDefined(); }); diff --git a/tests/unit/client-multikey.test.ts b/tests/unit/client-multikey.test.ts index 91fe276..3f756a6 100644 --- a/tests/unit/client-multikey.test.ts +++ b/tests/unit/client-multikey.test.ts @@ -1,6 +1,10 @@ /** * Unit tests for multi-API key functionality * Tests lazy getter validation and API key fallback chain + * + * API key architecture: + * - apiKey: for fiscal document operations (NFS-e, Companies, etc.) + * - dataApiKey: for all data/query services (Addresses, CT-e, CNPJ, CPF) */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; @@ -13,7 +17,7 @@ describe('NfeClient Multi-API Key Support', () => { beforeEach(() => { // Clear all NFE environment variables before each test delete process.env.NFE_API_KEY; - delete process.env.NFE_ADDRESS_API_KEY; + delete process.env.NFE_DATA_API_KEY; }); afterEach(() => { @@ -58,7 +62,7 @@ describe('NfeClient Multi-API Key Support', () => { const client = new NfeClient({}); expect(() => client.addresses).toThrow(ConfigurationError); - expect(() => client.addresses).toThrow(/addressApiKey|apiKey/); + expect(() => client.addresses).toThrow(/dataApiKey|apiKey/); }); it('should allow creating client without any apiKey', () => { @@ -94,9 +98,9 @@ describe('NfeClient Multi-API Key Support', () => { }); }); - describe('API key fallback chain for Address API', () => { - it('should use addressApiKey from config', () => { - const client = new NfeClient({ addressApiKey: 'address-key' }); + describe('API key fallback chain for data services (Addresses)', () => { + it('should use dataApiKey from config', () => { + const client = new NfeClient({ dataApiKey: 'data-key' }); expect(() => client.addresses).not.toThrow(); }); @@ -104,12 +108,12 @@ describe('NfeClient Multi-API Key Support', () => { it('should fall back to apiKey from config', () => { const client = new NfeClient({ apiKey: 'main-key' }); - // Should use main apiKey for addresses when addressApiKey not specified + // Should use main apiKey for addresses when dataApiKey not specified expect(() => client.addresses).not.toThrow(); }); - it('should fall back to NFE_ADDRESS_API_KEY environment variable', () => { - process.env.NFE_ADDRESS_API_KEY = 'env-address-key'; + it('should fall back to NFE_DATA_API_KEY environment variable', () => { + process.env.NFE_DATA_API_KEY = 'env-data-key'; const client = new NfeClient({}); expect(() => client.addresses).not.toThrow(); @@ -122,67 +126,150 @@ describe('NfeClient Multi-API Key Support', () => { expect(() => client.addresses).not.toThrow(); }); - it('should prefer addressApiKey over apiKey', () => { + it('should prefer dataApiKey over apiKey', () => { const client = new NfeClient({ apiKey: 'main-key', - addressApiKey: 'address-key', + dataApiKey: 'data-key', }); expect(() => client.addresses).not.toThrow(); const config = client.getConfig(); - expect(config.addressApiKey).toBe('address-key'); + expect(config.dataApiKey).toBe('data-key'); }); it('should prefer config keys over environment variables', () => { - process.env.NFE_ADDRESS_API_KEY = 'env-address-key'; + process.env.NFE_DATA_API_KEY = 'env-data-key'; process.env.NFE_API_KEY = 'env-main-key'; - const client = new NfeClient({ addressApiKey: 'config-address-key' }); + const client = new NfeClient({ dataApiKey: 'config-data-key' }); expect(() => client.addresses).not.toThrow(); const config = client.getConfig(); - expect(config.addressApiKey).toBe('config-address-key'); + expect(config.dataApiKey).toBe('config-data-key'); + }); + }); + + describe('API key fallback chain for data services (CT-e)', () => { + it('should use dataApiKey from config', () => { + const client = new NfeClient({ dataApiKey: 'data-key' }); + + expect(() => client.transportationInvoices).not.toThrow(); + }); + + it('should fall back to apiKey from config', () => { + const client = new NfeClient({ apiKey: 'main-key' }); + + // Should use main apiKey for CTE when dataApiKey not specified + expect(() => client.transportationInvoices).not.toThrow(); + }); + + it('should fall back to NFE_DATA_API_KEY environment variable', () => { + process.env.NFE_DATA_API_KEY = 'env-data-key'; + const client = new NfeClient({}); + + expect(() => client.transportationInvoices).not.toThrow(); + }); + + it('should fall back to NFE_API_KEY environment variable', () => { + process.env.NFE_API_KEY = 'env-main-key'; + const client = new NfeClient({}); + + expect(() => client.transportationInvoices).not.toThrow(); + }); + + it('should prefer dataApiKey over apiKey', () => { + const client = new NfeClient({ + apiKey: 'main-key', + dataApiKey: 'data-key', + }); + + expect(() => client.transportationInvoices).not.toThrow(); + const config = client.getConfig(); + expect(config.dataApiKey).toBe('data-key'); + }); + + it('should prefer config keys over environment variables', () => { + process.env.NFE_DATA_API_KEY = 'env-data-key'; + process.env.NFE_API_KEY = 'env-main-key'; + + const client = new NfeClient({ dataApiKey: 'config-data-key' }); + + expect(() => client.transportationInvoices).not.toThrow(); + const config = client.getConfig(); + expect(config.dataApiKey).toBe('config-data-key'); + }); + + it('should throw ConfigurationError when accessing transportationInvoices without any apiKey', () => { + const client = new NfeClient({}); + + expect(() => client.transportationInvoices).toThrow(ConfigurationError); + expect(() => client.transportationInvoices).toThrow(/dataApiKey|apiKey/); + }); + }); + + describe('both data services resolve same key', () => { + it('should use the same dataApiKey for both addresses and transportationInvoices', () => { + const client = new NfeClient({ dataApiKey: 'shared-data-key' }); + + // Both should work with the same key + expect(() => client.addresses).not.toThrow(); + expect(() => client.transportationInvoices).not.toThrow(); + + // Verify config has the shared key + const config = client.getConfig(); + expect(config.dataApiKey).toBe('shared-data-key'); + }); + + it('should use NFE_DATA_API_KEY env var for both addresses and transportationInvoices', () => { + process.env.NFE_DATA_API_KEY = 'env-shared-key'; + const client = new NfeClient({}); + + expect(() => client.addresses).not.toThrow(); + expect(() => client.transportationInvoices).not.toThrow(); }); }); describe('isolated resource usage', () => { - it('should allow using only addresses with addressApiKey (no apiKey)', () => { - const client = new NfeClient({ addressApiKey: 'address-only-key' }); + it('should allow using only data services with dataApiKey (no apiKey)', () => { + const client = new NfeClient({ dataApiKey: 'data-only-key' }); - // Addresses should work + // Data services should work expect(() => client.addresses).not.toThrow(); + expect(() => client.transportationInvoices).not.toThrow(); - // Other resources should throw + // Fiscal resources should throw expect(() => client.serviceInvoices).toThrow(ConfigurationError); expect(() => client.companies).toThrow(ConfigurationError); }); - it('should allow using only main resources with apiKey (no addressApiKey)', () => { + it('should allow using only main resources with apiKey (no dataApiKey)', () => { const client = new NfeClient({ apiKey: 'main-only-key' }); // Main resources should work expect(() => client.serviceInvoices).not.toThrow(); expect(() => client.companies).not.toThrow(); - // Addresses should also work (falls back to apiKey) + // Data services should also work (falls back to apiKey) expect(() => client.addresses).not.toThrow(); + expect(() => client.transportationInvoices).not.toThrow(); }); - it('should support separate API keys for different resources', () => { + it('should support separate API keys for data and main resources', () => { const client = new NfeClient({ apiKey: 'main-api-key', - addressApiKey: 'separate-address-key', + dataApiKey: 'separate-data-key', }); // All resources should work expect(() => client.serviceInvoices).not.toThrow(); expect(() => client.companies).not.toThrow(); expect(() => client.addresses).not.toThrow(); + expect(() => client.transportationInvoices).not.toThrow(); // Verify config has both keys const config = client.getConfig(); expect(config.apiKey).toBe('main-api-key'); - expect(config.addressApiKey).toBe('separate-address-key'); + expect(config.dataApiKey).toBe('separate-data-key'); }); }); @@ -197,7 +284,7 @@ describe('NfeClient Multi-API Key Support', () => { }); it('should cache addresses resource', () => { - const client = new NfeClient({ addressApiKey: 'test-key' }); + const client = new NfeClient({ dataApiKey: 'test-key' }); const addresses1 = client.addresses; const addresses2 = client.addresses; @@ -205,6 +292,15 @@ describe('NfeClient Multi-API Key Support', () => { expect(addresses1).toBe(addresses2); }); + it('should cache transportationInvoices resource', () => { + const client = new NfeClient({ dataApiKey: 'test-key' }); + + const transportationInvoices1 = client.transportationInvoices; + const transportationInvoices2 = client.transportationInvoices; + + expect(transportationInvoices1).toBe(transportationInvoices2); + }); + it('should clear cache on updateConfig', () => { const client = new NfeClient({ apiKey: 'initial-key' }); @@ -217,6 +313,19 @@ describe('NfeClient Multi-API Key Support', () => { // Resource should be a new instance expect(serviceInvoices1).not.toBe(serviceInvoices2); }); + + it('should clear data service cache on updateConfig with dataApiKey', () => { + const client = new NfeClient({ dataApiKey: 'initial-key' }); + + const transportationInvoices1 = client.transportationInvoices; + + client.updateConfig({ dataApiKey: 'new-key' }); + + const transportationInvoices2 = client.transportationInvoices; + + // Resource should be a new instance + expect(transportationInvoices1).not.toBe(transportationInvoices2); + }); }); describe('error messages', () => { @@ -228,12 +337,36 @@ describe('NfeClient Multi-API Key Support', () => { ); }); - it('should have descriptive error for missing address API key', () => { + it('should have descriptive error for missing data API key (addresses)', () => { const client = new NfeClient({}); expect(() => client.addresses).toThrow( - /addressApiKey|apiKey/ + /dataApiKey|apiKey/ + ); + }); + + it('should have descriptive error for missing data API key (transportationInvoices)', () => { + const client = new NfeClient({}); + + expect(() => client.transportationInvoices).toThrow( + /dataApiKey|apiKey/ ); }); + + it('should not recognize old NFE_ADDRESS_API_KEY environment variable', () => { + (process.env as Record).NFE_ADDRESS_API_KEY = 'old-key'; + const client = new NfeClient({}); + + // Should throw because NFE_ADDRESS_API_KEY is no longer recognized + expect(() => client.addresses).toThrow(ConfigurationError); + }); + + it('should not recognize old NFE_CTE_API_KEY environment variable', () => { + (process.env as Record).NFE_CTE_API_KEY = 'old-key'; + const client = new NfeClient({}); + + // Should throw because NFE_CTE_API_KEY is no longer recognized + expect(() => client.transportationInvoices).toThrow(ConfigurationError); + }); }); }); diff --git a/tests/unit/consumer-invoice-query.test.ts b/tests/unit/consumer-invoice-query.test.ts new file mode 100644 index 0000000..432f895 --- /dev/null +++ b/tests/unit/consumer-invoice-query.test.ts @@ -0,0 +1,275 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { ConsumerInvoiceQueryResource } from '../../src/core/resources/consumer-invoice-query.js'; +import { NfeClient } from '../../src/core/client.js'; +import { ConfigurationError, ValidationError } from '../../src/core/errors/index.js'; +import type { HttpClient } from '../../src/core/http/client.js'; +import type { HttpResponse, TaxCoupon } from '../../src/core/types.js'; + +// ============================================================================ +// Test Data +// ============================================================================ + +const VALID_ACCESS_KEY = '35240112345678000190590000000012341234567890'; + +const createMockCoupon = (overrides: Partial = {}): TaxCoupon => ({ + currentStatus: 'Authorized', + number: 12345, + satSerie: '900000001', + softwareVersion: '01.00.00', + accessKey: VALID_ACCESS_KEY, + cashier: 1, + issuedOn: '2024-01-15T10:30:00Z', + createdOn: '2024-01-15T11:00:00Z', + xmlVersion: '0.08', + issuer: { + federalTaxNumber: 12345678000190, + type: 'LegalEntity', + name: 'Empresa Teste Ltda', + tradeName: 'Empresa Teste', + stateTaxNumber: '123456789', + taxRegime: 'National_Simple', + }, + buyer: { + federalTaxNumber: 12345678901, + name: 'João da Silva', + }, + totals: { + icms: { + productAmount: 100.0, + icmsAmount: 18.0, + }, + couponAmount: 100.0, + totalAmount: 12.0, + }, + items: [ + { + description: 'Produto Teste', + quantity: 1, + unit: 'UN', + code: '001', + ncm: '12345678', + cfop: 5102, + unitAmount: 100.0, + netAmount: 100.0, + grossAmount: 100.0, + tax: { + totalTax: 12.0, + icms: { origin: '0', cst: '00', amount: 18.0, rate: 18.0 }, + }, + }, + ], + payment: { + payBack: 0, + paymentDetails: [ + { method: 'Cash', amount: 100.0 }, + ], + }, + ...overrides, +}); + +// ============================================================================ +// Tests +// ============================================================================ + +describe('ConsumerInvoiceQueryResource', () => { + let mockHttpClient: HttpClient; + let resource: ConsumerInvoiceQueryResource; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + getBuffer: vi.fn(), + } as unknown as HttpClient; + + resource = new ConsumerInvoiceQueryResource(mockHttpClient); + }); + + // -------------------------------------------------------------------------- + // Access Key Validation + // -------------------------------------------------------------------------- + + describe('access key validation', () => { + it('should reject empty access key', async () => { + await expect(resource.retrieve('')).rejects.toThrow(ValidationError); + await expect(resource.retrieve('')).rejects.toThrow('Access key is required'); + }); + + it('should reject whitespace-only access key', async () => { + await expect(resource.retrieve(' ')).rejects.toThrow(ValidationError); + await expect(resource.retrieve(' ')).rejects.toThrow('Access key is required'); + }); + + it('should reject non-numeric access key', async () => { + await expect(resource.retrieve('3524011234567800019059000000001234123456789X')).rejects.toThrow(ValidationError); + await expect(resource.retrieve('3524011234567800019059000000001234123456789X')).rejects.toThrow('Expected 44 numeric digits'); + }); + + it('should reject access key with wrong length (43 digits)', async () => { + await expect(resource.retrieve('3524011234567800019059000000001234123456789')).rejects.toThrow(ValidationError); + await expect(resource.retrieve('3524011234567800019059000000001234123456789')).rejects.toThrow('Expected 44 numeric digits'); + }); + + it('should reject access key with wrong length (45 digits)', async () => { + await expect(resource.retrieve('352401123456780001905900000000123412345678901')).rejects.toThrow(ValidationError); + }); + + it('should accept valid 44-digit numeric access key', async () => { + const mockResponse: HttpResponse = { + data: createMockCoupon(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await expect(resource.retrieve(VALID_ACCESS_KEY)).resolves.not.toThrow(); + }); + + it('should apply validation to downloadXml as well', async () => { + await expect(resource.downloadXml('')).rejects.toThrow(ValidationError); + await expect(resource.downloadXml('abc')).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // retrieve() + // -------------------------------------------------------------------------- + + describe('retrieve', () => { + it('should retrieve coupon details by access key', async () => { + const mockCoupon = createMockCoupon(); + const mockResponse: HttpResponse = { + data: mockCoupon, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.retrieve(VALID_ACCESS_KEY); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/consumerinvoices/coupon/${VALID_ACCESS_KEY}` + ); + expect(result).toEqual(mockCoupon); + expect(result.currentStatus).toBe('Authorized'); + expect(result.issuer?.name).toBe('Empresa Teste Ltda'); + expect(result.items).toHaveLength(1); + expect(result.totals?.couponAmount).toBe(100.0); + }); + + it('should propagate NotFoundError on 404', async () => { + const notFoundError = new Error('Not Found'); + (notFoundError as any).statusCode = 404; + vi.mocked(mockHttpClient.get).mockRejectedValue(notFoundError); + + await expect(resource.retrieve(VALID_ACCESS_KEY)).rejects.toThrow(); + }); + + it('should propagate AuthenticationError on 401', async () => { + const authError = new Error('Unauthorized'); + (authError as any).statusCode = 401; + vi.mocked(mockHttpClient.get).mockRejectedValue(authError); + + await expect(resource.retrieve(VALID_ACCESS_KEY)).rejects.toThrow(); + }); + + it('should trim whitespace from access key', async () => { + const mockResponse: HttpResponse = { + data: createMockCoupon(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.retrieve(` ${VALID_ACCESS_KEY} `); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/consumerinvoices/coupon/${VALID_ACCESS_KEY}` + ); + }); + }); + + // -------------------------------------------------------------------------- + // downloadXml() + // -------------------------------------------------------------------------- + + describe('downloadXml', () => { + it('should download XML as Buffer', async () => { + const xmlContent = Buffer.from(''); + const mockResponse = { + data: xmlContent, + status: 200, + headers: { 'content-type': 'application/xml' }, + }; + vi.mocked(mockHttpClient.getBuffer).mockResolvedValue(mockResponse); + + const result = await resource.downloadXml(VALID_ACCESS_KEY); + + expect(mockHttpClient.getBuffer).toHaveBeenCalledWith( + `/v1/consumerinvoices/coupon/${VALID_ACCESS_KEY}.xml`, + 'application/xml' + ); + expect(result).toBeInstanceOf(Buffer); + expect(result.toString()).toContain(''); + }); + + it('should propagate NotFoundError on 404', async () => { + const notFoundError = new Error('Not Found'); + (notFoundError as any).statusCode = 404; + vi.mocked(mockHttpClient.getBuffer).mockRejectedValue(notFoundError); + + await expect(resource.downloadXml(VALID_ACCESS_KEY)).rejects.toThrow(); + }); + }); +}); + +// ============================================================================ +// NfeClient Integration +// ============================================================================ + +describe('NfeClient.consumerInvoiceQuery', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + delete process.env.NFE_API_KEY; + delete process.env.NFE_DATA_API_KEY; + }); + + afterEach(() => { + process.env = { ...originalEnv }; + vi.restoreAllMocks(); + }); + + it('should lazily initialize consumerInvoiceQuery on first access', () => { + const client = new NfeClient({ apiKey: 'test-key' }); + + const resource1 = client.consumerInvoiceQuery; + const resource2 = client.consumerInvoiceQuery; + + expect(resource1).toBeInstanceOf(ConsumerInvoiceQueryResource); + expect(resource1).toBe(resource2); // Same cached instance + }); + + it('should throw ConfigurationError when no API key is configured', () => { + const client = new NfeClient({}); + + expect(() => client.consumerInvoiceQuery).toThrow(ConfigurationError); + expect(() => client.consumerInvoiceQuery).toThrow(/API key required/); + }); + + it('should work with dataApiKey', () => { + const client = new NfeClient({ dataApiKey: 'data-key-only' }); + + expect(() => client.consumerInvoiceQuery).not.toThrow(); + expect(client.consumerInvoiceQuery).toBeInstanceOf(ConsumerInvoiceQueryResource); + }); + + it('should fall back to apiKey when dataApiKey is not set', () => { + const client = new NfeClient({ apiKey: 'main-key' }); + + expect(() => client.consumerInvoiceQuery).not.toThrow(); + expect(client.consumerInvoiceQuery).toBeInstanceOf(ConsumerInvoiceQueryResource); + }); +}); diff --git a/tests/unit/http-client.test.ts b/tests/unit/http-client.test.ts index b289d2e..a199978 100644 --- a/tests/unit/http-client.test.ts +++ b/tests/unit/http-client.test.ts @@ -605,6 +605,56 @@ it.skip('should include Basic Auth header', async () => { }); }); + describe('getBuffer', () => { + it('should make GET request with custom Accept header and return Buffer', async () => { + const binaryContent = new Uint8Array([0x25, 0x50, 0x44, 0x46]); // %PDF + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: createMockHeaders([['content-type', 'application/pdf']]), + arrayBuffer: async () => binaryContent.buffer, + }); + + const response = await httpClient.getBuffer('/test.pdf', 'application/pdf'); + + expect(response.status).toBe(200); + expect(Buffer.isBuffer(response.data)).toBe(true); + const callArgs = fetchMock.mock.calls[0]; + const headers = callArgs[1].headers; + // Headers may be a Headers object or plain object depending on implementation + const acceptValue = typeof headers.get === 'function' + ? headers.get('Accept') + : headers['Accept'] || headers['accept']; + expect(acceptValue).toBe('application/pdf'); + }); + + it('should handle application/xml Accept header', async () => { + const xmlContent = new TextEncoder().encode(''); + fetchMock.mockResolvedValue({ + ok: true, + status: 200, + statusText: 'OK', + headers: createMockHeaders([['content-type', 'application/xml']]), + arrayBuffer: async () => xmlContent.buffer, + }); + + const response = await httpClient.getBuffer('/test.xml', 'application/xml'); + + expect(response.status).toBe(200); + expect(Buffer.isBuffer(response.data)).toBe(true); + }); + + it('should propagate errors on getBuffer', async () => { + fetchMock.mockResolvedValue( + createMockErrorResponse(404, 'Not Found', { message: 'Resource not found' }) + ); + + await expect(httpClient.getBuffer('/missing.pdf', 'application/pdf')) + .rejects.toThrow(); + }); + }); + describe('Utility Functions', () => { it('should create default retry config', () => { const retryConfig = createDefaultRetryConfig(); diff --git a/tests/unit/legal-entity-lookup.test.ts b/tests/unit/legal-entity-lookup.test.ts new file mode 100644 index 0000000..0992bf2 --- /dev/null +++ b/tests/unit/legal-entity-lookup.test.ts @@ -0,0 +1,587 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { LegalEntityLookupResource, LEGAL_ENTITY_API_BASE_URL } from '../../src/core/resources/legal-entity-lookup.js'; +import { ValidationError } from '../../src/core/errors/index.js'; +import type { HttpClient } from '../../src/core/http/client.js'; +import type { + HttpResponse, + LegalEntityBasicInfoResponse, + LegalEntityStateTaxResponse, + LegalEntityStateTaxForInvoiceResponse, +} from '../../src/core/types.js'; + +// ============================================================================ +// Test Data +// ============================================================================ + +const VALID_CNPJ_DIGITS = '12345678000190'; +const VALID_CNPJ_FORMATTED = '12.345.678/0001-90'; + +const createMockBasicInfoResponse = ( + overrides: Partial = {} +): LegalEntityBasicInfoResponse => ({ + legalEntity: { + tradeName: 'Empresa Teste', + name: 'EMPRESA TESTE LTDA', + federalTaxNumber: 12345678000190, + size: 'ME', + openedOn: '2020-01-15', + status: 'Active', + email: 'contato@empresa.com.br', + unit: 'Headoffice', + shareCapital: 100000, + address: { + state: 'SP', + city: { code: '3550308', name: 'São Paulo' }, + district: 'Centro', + street: 'Rua Principal', + number: '100', + postalCode: '01001000', + country: 'Brasil', + }, + phones: [{ ddd: '11', number: '99999999', source: 'RFB' }], + economicActivities: [ + { type: 'Main', code: 6201500, description: 'Desenvolvimento de software' }, + ], + legalNature: { code: '2062', description: 'Sociedade Empresária Limitada' }, + partners: [ + { name: 'João Silva', qualification: { code: '49', description: 'Sócio-Administrador' } }, + ], + }, + ...overrides, +}); + +const createMockStateTaxResponse = ( + overrides: Partial = {} +): LegalEntityStateTaxResponse => ({ + legalEntity: { + tradeName: 'Empresa Teste', + name: 'EMPRESA TESTE LTDA', + federalTaxNumber: 12345678000190, + taxRegime: 'SimplesNacional', + legalNature: 'SociedadeEmpresariaLimitada', + fiscalUnit: 'SP', + checkCode: 'ABC123', + stateTaxes: [ + { + status: 'Abled', + taxNumber: '123456789012', + openedOn: '2020-01-15', + code: 'SP', + nfe: { status: 'Abled', description: 'Contribuinte' }, + nfse: { status: 'Unknown', description: 'Não habilitado' }, + cte: { status: 'Unabled' }, + nfce: { status: 'Unabled' }, + }, + ], + }, + ...overrides, +}); + +const createMockStateTaxForInvoiceResponse = ( + overrides: Partial = {} +): LegalEntityStateTaxForInvoiceResponse => ({ + legalEntity: { + tradeName: 'Empresa Teste', + name: 'EMPRESA TESTE LTDA', + federalTaxNumber: 12345678000190, + taxRegime: 'SimplesNacional', + legalNature: 'SociedadeEmpresariaLimitada', + fiscalUnit: 'SP', + checkCode: 'ABC123', + stateTaxes: [ + { + status: 'Abled', + taxNumber: '123456789012', + openedOn: '2020-01-15', + code: 'SP', + nfe: { status: 'Abled', description: 'Contribuinte' }, + nfse: { status: 'Unknown' }, + cte: { status: 'Unabled' }, + nfce: { status: 'Unabled' }, + }, + ], + }, + ...overrides, +}); + +// ============================================================================ +// Tests +// ============================================================================ + +describe('LegalEntityLookupResource', () => { + let mockHttpClient: HttpClient; + let resource: LegalEntityLookupResource; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + getBuffer: vi.fn(), + } as unknown as HttpClient; + + resource = new LegalEntityLookupResource(mockHttpClient); + }); + + // -------------------------------------------------------------------------- + // Constants + // -------------------------------------------------------------------------- + + describe('LEGAL_ENTITY_API_BASE_URL', () => { + it('should export the correct base URL', () => { + expect(LEGAL_ENTITY_API_BASE_URL).toBe('https://legalentity.api.nfe.io'); + }); + }); + + // -------------------------------------------------------------------------- + // CNPJ Validation + // -------------------------------------------------------------------------- + + describe('CNPJ validation', () => { + it('should reject empty CNPJ', async () => { + await expect(resource.getBasicInfo('')).rejects.toThrow(ValidationError); + await expect(resource.getBasicInfo('')).rejects.toThrow('Federal tax number (CNPJ) is required'); + }); + + it('should reject whitespace-only CNPJ', async () => { + await expect(resource.getBasicInfo(' ')).rejects.toThrow(ValidationError); + await expect(resource.getBasicInfo(' ')).rejects.toThrow('Federal tax number (CNPJ) is required'); + }); + + it('should reject CNPJ with fewer than 14 digits', async () => { + await expect(resource.getBasicInfo('1234567800019')).rejects.toThrow(ValidationError); + await expect(resource.getBasicInfo('1234567800019')).rejects.toThrow('Expected 14 digits'); + }); + + it('should reject CNPJ with more than 14 digits', async () => { + await expect(resource.getBasicInfo('123456780001901')).rejects.toThrow(ValidationError); + await expect(resource.getBasicInfo('123456780001901')).rejects.toThrow('Expected 14 digits'); + }); + + it('should accept valid 14-digit CNPJ', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await expect(resource.getBasicInfo(VALID_CNPJ_DIGITS)).resolves.not.toThrow(); + }); + + it('should accept formatted CNPJ and strip punctuation', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getBasicInfo(VALID_CNPJ_FORMATTED); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/basicInfo/${VALID_CNPJ_DIGITS}`, + undefined + ); + }); + + it('should apply CNPJ validation to getStateTaxInfo', async () => { + await expect(resource.getStateTaxInfo('SP', '')).rejects.toThrow(ValidationError); + await expect(resource.getStateTaxInfo('SP', '123')).rejects.toThrow(ValidationError); + }); + + it('should apply CNPJ validation to getStateTaxForInvoice', async () => { + await expect(resource.getStateTaxForInvoice('SP', '')).rejects.toThrow(ValidationError); + }); + + it('should apply CNPJ validation to getSuggestedStateTaxForInvoice', async () => { + await expect(resource.getSuggestedStateTaxForInvoice('SP', '')).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // State Code Validation + // -------------------------------------------------------------------------- + + describe('state code validation', () => { + it('should reject empty state code', async () => { + await expect(resource.getStateTaxInfo('', VALID_CNPJ_DIGITS)).rejects.toThrow(ValidationError); + await expect(resource.getStateTaxInfo('', VALID_CNPJ_DIGITS)).rejects.toThrow('State code is required'); + }); + + it('should reject whitespace-only state code', async () => { + await expect(resource.getStateTaxInfo(' ', VALID_CNPJ_DIGITS)).rejects.toThrow(ValidationError); + }); + + it('should reject invalid state code', async () => { + await expect(resource.getStateTaxInfo('XX', VALID_CNPJ_DIGITS)).rejects.toThrow(ValidationError); + await expect(resource.getStateTaxInfo('XX', VALID_CNPJ_DIGITS)).rejects.toThrow('Invalid state code'); + }); + + it('should accept valid uppercase state code', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await expect(resource.getStateTaxInfo('SP', VALID_CNPJ_DIGITS)).resolves.not.toThrow(); + }); + + it('should accept lowercase state code and normalize to uppercase', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStateTaxInfo('sp', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxInfo/SP/${VALID_CNPJ_DIGITS}` + ); + }); + + it('should accept mixed-case state code', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStateTaxInfo('Rj', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxInfo/RJ/${VALID_CNPJ_DIGITS}` + ); + }); + + it('should accept EX and NA special codes', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await expect(resource.getStateTaxInfo('EX', VALID_CNPJ_DIGITS)).resolves.not.toThrow(); + }); + + it('should apply state validation to getStateTaxForInvoice', async () => { + await expect(resource.getStateTaxForInvoice('XX', VALID_CNPJ_DIGITS)).rejects.toThrow(ValidationError); + }); + + it('should apply state validation to getSuggestedStateTaxForInvoice', async () => { + await expect(resource.getSuggestedStateTaxForInvoice('XX', VALID_CNPJ_DIGITS)).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // getBasicInfo() + // -------------------------------------------------------------------------- + + describe('getBasicInfo', () => { + it('should retrieve basic info by CNPJ', async () => { + const mockData = createMockBasicInfoResponse(); + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getBasicInfo(VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/basicInfo/${VALID_CNPJ_DIGITS}`, + undefined + ); + expect(result.legalEntity?.name).toBe('EMPRESA TESTE LTDA'); + expect(result.legalEntity?.tradeName).toBe('Empresa Teste'); + expect(result.legalEntity?.status).toBe('Active'); + expect(result.legalEntity?.address?.city?.name).toBe('São Paulo'); + expect(result.legalEntity?.economicActivities).toHaveLength(1); + expect(result.legalEntity?.partners).toHaveLength(1); + }); + + it('should pass updateAddress query parameter when specified', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getBasicInfo(VALID_CNPJ_DIGITS, { updateAddress: false }); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/basicInfo/${VALID_CNPJ_DIGITS}`, + { updateAddress: false } + ); + }); + + it('should pass updateCityCode query parameter when specified', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getBasicInfo(VALID_CNPJ_DIGITS, { updateCityCode: true }); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/basicInfo/${VALID_CNPJ_DIGITS}`, + { updateCityCode: true } + ); + }); + + it('should pass both options when both are specified', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getBasicInfo(VALID_CNPJ_DIGITS, { + updateAddress: false, + updateCityCode: true, + }); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/basicInfo/${VALID_CNPJ_DIGITS}`, + { updateAddress: false, updateCityCode: true } + ); + }); + + it('should not pass params when no options are given', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getBasicInfo(VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/basicInfo/${VALID_CNPJ_DIGITS}`, + undefined + ); + }); + + it('should strip formatting from CNPJ in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockBasicInfoResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getBasicInfo('12.345.678/0001-90'); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + '/v2/legalentities/basicInfo/12345678000190', + undefined + ); + }); + }); + + // -------------------------------------------------------------------------- + // getStateTaxInfo() + // -------------------------------------------------------------------------- + + describe('getStateTaxInfo', () => { + it('should retrieve state tax info by state and CNPJ', async () => { + const mockData = createMockStateTaxResponse(); + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getStateTaxInfo('SP', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxInfo/SP/${VALID_CNPJ_DIGITS}` + ); + expect(result.legalEntity?.taxRegime).toBe('SimplesNacional'); + expect(result.legalEntity?.stateTaxes).toHaveLength(1); + expect(result.legalEntity?.stateTaxes?.[0]?.status).toBe('Abled'); + expect(result.legalEntity?.stateTaxes?.[0]?.taxNumber).toBe('123456789012'); + }); + + it('should normalize state to uppercase in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStateTaxInfo('mg', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxInfo/MG/${VALID_CNPJ_DIGITS}` + ); + }); + + it('should strip CNPJ formatting in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStateTaxInfo('SP', '12.345.678/0001-90'); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxInfo/SP/${VALID_CNPJ_DIGITS}` + ); + }); + }); + + // -------------------------------------------------------------------------- + // getStateTaxForInvoice() + // -------------------------------------------------------------------------- + + describe('getStateTaxForInvoice', () => { + it('should retrieve state tax for invoice by state and CNPJ', async () => { + const mockData = createMockStateTaxForInvoiceResponse(); + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getStateTaxForInvoice('SP', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxForInvoice/SP/${VALID_CNPJ_DIGITS}` + ); + expect(result.legalEntity?.stateTaxes).toHaveLength(1); + expect(result.legalEntity?.stateTaxes?.[0]?.status).toBe('Abled'); + }); + + it('should normalize state to uppercase in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxForInvoiceResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStateTaxForInvoice('rj', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxForInvoice/RJ/${VALID_CNPJ_DIGITS}` + ); + }); + }); + + // -------------------------------------------------------------------------- + // getSuggestedStateTaxForInvoice() + // -------------------------------------------------------------------------- + + describe('getSuggestedStateTaxForInvoice', () => { + it('should retrieve suggested state tax for invoice by state and CNPJ', async () => { + const mockData = createMockStateTaxForInvoiceResponse(); + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getSuggestedStateTaxForInvoice('SP', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxSuggestedForInvoice/SP/${VALID_CNPJ_DIGITS}` + ); + expect(result.legalEntity?.stateTaxes).toHaveLength(1); + }); + + it('should use a different URL path from getStateTaxForInvoice', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxForInvoiceResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getSuggestedStateTaxForInvoice('SP', VALID_CNPJ_DIGITS); + + const calledUrl = vi.mocked(mockHttpClient.get).mock.calls[0]?.[0]; + expect(calledUrl).toContain('stateTaxSuggestedForInvoice'); + expect(calledUrl).not.toContain('stateTaxForInvoice/'); + }); + + it('should normalize state to uppercase in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockStateTaxForInvoiceResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getSuggestedStateTaxForInvoice('ba', VALID_CNPJ_DIGITS); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/legalentities/stateTaxSuggestedForInvoice/BA/${VALID_CNPJ_DIGITS}` + ); + }); + }); + + // -------------------------------------------------------------------------- + // Error Handling + // -------------------------------------------------------------------------- + + describe('error handling', () => { + it('should propagate errors from HTTP client on 404', async () => { + const notFoundError = new Error('Not Found'); + (notFoundError as any).statusCode = 404; + vi.mocked(mockHttpClient.get).mockRejectedValue(notFoundError); + + await expect(resource.getBasicInfo(VALID_CNPJ_DIGITS)).rejects.toThrow(); + }); + + it('should propagate errors from HTTP client on 400', async () => { + const badRequestError = new Error('Bad Request'); + (badRequestError as any).statusCode = 400; + vi.mocked(mockHttpClient.get).mockRejectedValue(badRequestError); + + await expect(resource.getBasicInfo(VALID_CNPJ_DIGITS)).rejects.toThrow(); + }); + + it('should propagate authentication errors on 401', async () => { + const authError = new Error('Unauthorized'); + (authError as any).statusCode = 401; + vi.mocked(mockHttpClient.get).mockRejectedValue(authError); + + await expect(resource.getStateTaxInfo('SP', VALID_CNPJ_DIGITS)).rejects.toThrow(); + }); + + it('should propagate server errors on 500', async () => { + const serverError = new Error('Internal Server Error'); + (serverError as any).statusCode = 500; + vi.mocked(mockHttpClient.get).mockRejectedValue(serverError); + + await expect(resource.getStateTaxForInvoice('SP', VALID_CNPJ_DIGITS)).rejects.toThrow(); + }); + + it('should propagate network errors', async () => { + const networkError = new Error('fetch failed'); + vi.mocked(mockHttpClient.get).mockRejectedValue(networkError); + + await expect(resource.getSuggestedStateTaxForInvoice('SP', VALID_CNPJ_DIGITS)).rejects.toThrow('fetch failed'); + }); + }); +}); diff --git a/tests/unit/natural-person-lookup.test.ts b/tests/unit/natural-person-lookup.test.ts new file mode 100644 index 0000000..2fdc867 --- /dev/null +++ b/tests/unit/natural-person-lookup.test.ts @@ -0,0 +1,300 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { NaturalPersonLookupResource, NATURAL_PERSON_API_BASE_URL } from '../../src/core/resources/natural-person-lookup.js'; +import { ValidationError } from '../../src/core/errors/index.js'; +import type { HttpClient } from '../../src/core/http/client.js'; +import type { + HttpResponse, + NaturalPersonStatusResponse, +} from '../../src/core/types.js'; + +// ============================================================================ +// Test Data +// ============================================================================ + +const VALID_CPF_DIGITS = '12345678901'; +const VALID_CPF_FORMATTED = '123.456.789-01'; +const VALID_BIRTH_DATE = '1990-01-15'; + +const createMockStatusResponse = ( + overrides: Partial = {} +): NaturalPersonStatusResponse => ({ + name: 'JOÃO DA SILVA', + federalTaxNumber: '12345678901', + birthOn: '1990-01-15T00:00:00', + status: 'Regular', + createdOn: '2026-02-15T10:30:00', + ...overrides, +}); + +// ============================================================================ +// Tests +// ============================================================================ + +describe('NaturalPersonLookupResource', () => { + let mockHttpClient: HttpClient; + let resource: NaturalPersonLookupResource; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + getBuffer: vi.fn(), + } as unknown as HttpClient; + + resource = new NaturalPersonLookupResource(mockHttpClient); + }); + + // -------------------------------------------------------------------------- + // Constants + // -------------------------------------------------------------------------- + + describe('NATURAL_PERSON_API_BASE_URL', () => { + it('should export the correct base URL', () => { + expect(NATURAL_PERSON_API_BASE_URL).toBe('https://naturalperson.api.nfe.io'); + }); + }); + + // -------------------------------------------------------------------------- + // CPF Validation + // -------------------------------------------------------------------------- + + describe('CPF validation', () => { + it('should reject empty CPF', async () => { + await expect(resource.getStatus('', VALID_BIRTH_DATE)).rejects.toThrow(ValidationError); + await expect(resource.getStatus('', VALID_BIRTH_DATE)).rejects.toThrow('Federal tax number (CPF) is required'); + }); + + it('should reject whitespace-only CPF', async () => { + await expect(resource.getStatus(' ', VALID_BIRTH_DATE)).rejects.toThrow(ValidationError); + await expect(resource.getStatus(' ', VALID_BIRTH_DATE)).rejects.toThrow('Federal tax number (CPF) is required'); + }); + + it('should reject CPF with fewer than 11 digits', async () => { + await expect(resource.getStatus('1234567890', VALID_BIRTH_DATE)).rejects.toThrow(ValidationError); + await expect(resource.getStatus('1234567890', VALID_BIRTH_DATE)).rejects.toThrow('Expected 11 digits'); + }); + + it('should reject CPF with more than 11 digits', async () => { + await expect(resource.getStatus('123456789012', VALID_BIRTH_DATE)).rejects.toThrow(ValidationError); + await expect(resource.getStatus('123456789012', VALID_BIRTH_DATE)).rejects.toThrow('Expected 11 digits'); + }); + + it('should accept valid 11-digit CPF', async () => { + const mockResponse: HttpResponse = { + data: createMockStatusResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await expect(resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE)).resolves.not.toThrow(); + }); + + it('should accept formatted CPF and strip punctuation', async () => { + const mockResponse: HttpResponse = { + data: createMockStatusResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStatus(VALID_CPF_FORMATTED, VALID_BIRTH_DATE); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/naturalperson/status/${VALID_CPF_DIGITS}/${VALID_BIRTH_DATE}` + ); + }); + }); + + // -------------------------------------------------------------------------- + // Birth Date Validation + // -------------------------------------------------------------------------- + + describe('Birth date validation', () => { + const setupMock = () => { + const mockResponse: HttpResponse = { + data: createMockStatusResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + }; + + it('should accept valid date string in YYYY-MM-DD format', async () => { + setupMock(); + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-01-15')).resolves.not.toThrow(); + }); + + it('should accept a Date object and convert to YYYY-MM-DD', async () => { + setupMock(); + // Use UTC date to avoid timezone issues + const date = new Date(Date.UTC(1990, 0, 15)); + await resource.getStatus(VALID_CPF_DIGITS, date); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/naturalperson/status/${VALID_CPF_DIGITS}/1990-01-15` + ); + }); + + it('should reject empty string birth date', async () => { + await expect(resource.getStatus(VALID_CPF_DIGITS, '')).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, '')).rejects.toThrow('Birth date is required'); + }); + + it('should reject invalid date format (DD/MM/YYYY)', async () => { + await expect(resource.getStatus(VALID_CPF_DIGITS, '15/01/1990')).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, '15/01/1990')).rejects.toThrow('Expected YYYY-MM-DD format'); + }); + + it('should reject invalid month (13)', async () => { + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-13-01')).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-13-01')).rejects.toThrow('Month must be between 01 and 12'); + }); + + it('should reject invalid day (32)', async () => { + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-01-32')).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-01-32')).rejects.toThrow('Day must be between 01 and 31'); + }); + + it('should reject month 00', async () => { + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-00-15')).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-00-15')).rejects.toThrow('Month must be between 01 and 12'); + }); + + it('should reject day 00', async () => { + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-01-00')).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, '1990-01-00')).rejects.toThrow('Day must be between 01 and 31'); + }); + + it('should reject invalid Date object', async () => { + const invalidDate = new Date('invalid'); + await expect(resource.getStatus(VALID_CPF_DIGITS, invalidDate)).rejects.toThrow(ValidationError); + await expect(resource.getStatus(VALID_CPF_DIGITS, invalidDate)).rejects.toThrow('invalid Date object'); + }); + }); + + // -------------------------------------------------------------------------- + // getStatus() - Successful calls + // -------------------------------------------------------------------------- + + describe('getStatus()', () => { + it('should send GET request to correct URL path', async () => { + const mockData = createMockStatusResponse(); + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/naturalperson/status/${VALID_CPF_DIGITS}/${VALID_BIRTH_DATE}` + ); + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(result).toEqual(mockData); + }); + + it('should return typed response with all fields', async () => { + const mockData = createMockStatusResponse(); + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE); + + expect(result.name).toBe('JOÃO DA SILVA'); + expect(result.federalTaxNumber).toBe('12345678901'); + expect(result.birthOn).toBe('1990-01-15T00:00:00'); + expect(result.status).toBe('Regular'); + expect(result.createdOn).toBe('2026-02-15T10:30:00'); + }); + + it('should normalize CPF punctuation in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockStatusResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + await resource.getStatus('123.456.789-01', VALID_BIRTH_DATE); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/naturalperson/status/12345678901/${VALID_BIRTH_DATE}` + ); + }); + + it('should convert Date object to YYYY-MM-DD string in URL', async () => { + const mockResponse: HttpResponse = { + data: createMockStatusResponse(), + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const date = new Date(Date.UTC(1990, 0, 15)); + await resource.getStatus(VALID_CPF_DIGITS, date); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v1/naturalperson/status/${VALID_CPF_DIGITS}/1990-01-15` + ); + }); + + it('should handle response with optional fields missing', async () => { + const mockData: NaturalPersonStatusResponse = { + federalTaxNumber: '12345678901', + }; + const mockResponse: HttpResponse = { + data: mockData, + status: 200, + headers: {}, + }; + vi.mocked(mockHttpClient.get).mockResolvedValue(mockResponse); + + const result = await resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE); + + expect(result.federalTaxNumber).toBe('12345678901'); + expect(result.name).toBeUndefined(); + expect(result.status).toBeUndefined(); + expect(result.birthOn).toBeUndefined(); + expect(result.createdOn).toBeUndefined(); + }); + }); + + // -------------------------------------------------------------------------- + // Error Handling + // -------------------------------------------------------------------------- + + describe('error handling', () => { + it('should propagate 404 NotFoundError from HttpClient', async () => { + const error = new Error('Not Found'); + error.name = 'NotFoundError'; + vi.mocked(mockHttpClient.get).mockRejectedValue(error); + + await expect(resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE)).rejects.toThrow('Not Found'); + }); + + it('should propagate 401 AuthenticationError from HttpClient', async () => { + const error = new Error('Unauthorized'); + error.name = 'AuthenticationError'; + vi.mocked(mockHttpClient.get).mockRejectedValue(error); + + await expect(resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE)).rejects.toThrow('Unauthorized'); + }); + + it('should propagate 400 BadRequestError from HttpClient', async () => { + const error = new Error('Bad Request'); + error.name = 'BadRequestError'; + vi.mocked(mockHttpClient.get).mockRejectedValue(error); + + await expect(resource.getStatus(VALID_CPF_DIGITS, VALID_BIRTH_DATE)).rejects.toThrow('Bad Request'); + }); + }); +}); diff --git a/tests/unit/nfe-client.test.ts b/tests/unit/nfe-client.test.ts index 1a2c0f7..779c781 100644 --- a/tests/unit/nfe-client.test.ts +++ b/tests/unit/nfe-client.test.ts @@ -116,6 +116,38 @@ describe('NfeClient', () => { expect(client.webhooks.update).toBeInstanceOf(Function); expect(client.webhooks.delete).toBeInstanceOf(Function); }); + + it('should have productInvoiceQuery resource', () => { + expect(client.productInvoiceQuery).toBeDefined(); + expect(client.productInvoiceQuery.retrieve).toBeInstanceOf(Function); + expect(client.productInvoiceQuery.downloadPdf).toBeInstanceOf(Function); + expect(client.productInvoiceQuery.downloadXml).toBeInstanceOf(Function); + expect(client.productInvoiceQuery.listEvents).toBeInstanceOf(Function); + }); + + it('should return same productInvoiceQuery instance on repeated access (lazy init)', () => { + const first = client.productInvoiceQuery; + const second = client.productInvoiceQuery; + expect(first).toBe(second); + }); + }); + + describe('productInvoiceQuery without api key', () => { + it('should throw ConfigurationError when no API key is available', () => { + // Clear env vars to ensure no fallback + const savedApiKey = process.env.NFE_API_KEY; + const savedDataKey = process.env.NFE_DATA_API_KEY; + delete process.env.NFE_API_KEY; + delete process.env.NFE_DATA_API_KEY; + + try { + const noKeyClient = new NfeClient({ apiKey: '' as any, environment: 'development' }); + expect(() => noKeyClient.productInvoiceQuery).toThrow(ConfigurationError); + } finally { + process.env.NFE_API_KEY = savedApiKey; + if (savedDataKey) process.env.NFE_DATA_API_KEY = savedDataKey; + } + }); }); describe('configuration validation', () => { diff --git a/tests/unit/resources/inbound-product-invoices.test.ts b/tests/unit/resources/inbound-product-invoices.test.ts new file mode 100644 index 0000000..ed43297 --- /dev/null +++ b/tests/unit/resources/inbound-product-invoices.test.ts @@ -0,0 +1,510 @@ +/** + * Unit tests for InboundProductInvoicesResource + * Tests NF-e distribution (Consulta NF-e Distribuição) operations + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { InboundProductInvoicesResource } from '../../../src/core/resources/inbound-product-invoices.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { + HttpResponse, + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings +} from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('InboundProductInvoicesResource', () => { + let resource: InboundProductInvoicesResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + }; + + // Valid 44-digit access key for testing + const validAccessKey = '35240112345678000190550010000001231234567890'; + const testCompanyId = 'company-123'; + const testEventKey = 'event-key-456'; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + resource = new InboundProductInvoicesResource(mockHttpClient as unknown as HttpClient); + }); + + // ========================================================================== + // Validation tests + // ========================================================================== + + describe('validation', () => { + it('should throw ValidationError for empty companyId on enableAutoFetch', async () => { + await expect(resource.enableAutoFetch('', {})).rejects.toThrow(ValidationError); + await expect(resource.enableAutoFetch('', {})).rejects.toThrow(/Company ID is required/); + }); + + it('should throw ValidationError for whitespace-only companyId', async () => { + await expect(resource.enableAutoFetch(' ', {})).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty companyId on disableAutoFetch', async () => { + await expect(resource.disableAutoFetch('')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty companyId on getSettings', async () => { + await expect(resource.getSettings('')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for invalid access key format', async () => { + await expect(resource.getDetails(testCompanyId, '12345')).rejects.toThrow(ValidationError); + await expect(resource.getDetails(testCompanyId, '12345')).rejects.toThrow(/Expected 44 numeric digits/); + }); + + it('should throw ValidationError for empty access key', async () => { + await expect(resource.getDetails(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.getDetails(testCompanyId, '')).rejects.toThrow(/Access key is required/); + }); + + it('should throw ValidationError for non-numeric access key', async () => { + await expect(resource.getDetails(testCompanyId, 'a'.repeat(44))).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty eventKey', async () => { + await expect(resource.getEventDetails(testCompanyId, validAccessKey, '')).rejects.toThrow(ValidationError); + await expect(resource.getEventDetails(testCompanyId, validAccessKey, '')).rejects.toThrow(/Event key is required/); + }); + + it('should throw ValidationError for whitespace-only eventKey', async () => { + await expect(resource.getEventDetails(testCompanyId, validAccessKey, ' ')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty accessKeyOrNsu on reprocessWebhook', async () => { + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(/Access key or NSU is required/); + }); + }); + + // ========================================================================== + // enableAutoFetch() tests + // ========================================================================== + + describe('enableAutoFetch', () => { + const mockSettings: InboundSettings = { + startFromNsu: '999999', + startFromDate: '2024-01-01T00:00:00Z', + environmentSEFAZ: 'Production', + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + webhookVersion: '2', + companyId: testCompanyId, + status: 'Active', + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-15T10:30:00Z', + }; + + it('should enable auto-fetch with provided options', async () => { + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const options = { + startFromNsu: '999999', + environmentSEFAZ: 'Production', + webhookVersion: '2', + }; + + const result = await resource.enableAutoFetch(testCompanyId, options); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoices`, + options + ); + }); + }); + + // ========================================================================== + // disableAutoFetch() tests + // ========================================================================== + + describe('disableAutoFetch', () => { + it('should disable auto-fetch for a company', async () => { + const mockSettings: InboundSettings = { + startFromNsu: '999999', + startFromDate: '2024-01-01T00:00:00Z', + environmentSEFAZ: null, + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + webhookVersion: '2', + companyId: testCompanyId, + status: 'Inactive', + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-16T10:30:00Z', + }; + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.delete.mockResolvedValue(mockResponse); + + const result = await resource.disableAutoFetch(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.delete).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoices` + ); + }); + }); + + // ========================================================================== + // getSettings() tests + // ========================================================================== + + describe('getSettings', () => { + it('should get current settings for a company', async () => { + const mockSettings: InboundSettings = { + startFromNsu: '999999', + startFromDate: '2024-01-01T00:00:00Z', + environmentSEFAZ: 'Production', + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + webhookVersion: '2', + companyId: testCompanyId, + status: 'Active', + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-15T10:30:00Z', + }; + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getSettings(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoices` + ); + }); + }); + + // ========================================================================== + // getDetails() / getProductInvoiceDetails() tests + // ========================================================================== + + describe('getDetails', () => { + const mockMetadata: InboundInvoiceMetadata = { + id: 'doc-123', + createdOn: '2024-01-15T10:30:00Z', + accessKey: validAccessKey, + parentAccessKey: '', + company: { id: testCompanyId, federalTaxNumber: '12345678000190' }, + issuer: { federalTaxNumber: '98765432000110', name: 'Issuer Corp' }, + buyer: { federalTaxNumber: '12345678000190', name: 'Buyer Corp' }, + transportation: { federalTaxNumber: '11111111000111', name: 'Transport Co' }, + links: { xml: 'https://example.com/xml', pdf: 'https://example.com/pdf' }, + xmlUrl: 'https://example.com/xml', + federalTaxNumberSender: '98765432000110', + nameSender: 'Issuer Corp', + type: null, + nsu: '12345', + nsuParent: '', + nfeNumber: '1001', + nfeSerialNumber: '1', + issuedOn: '2024-01-10T08:00:00Z', + description: 'Test invoice', + totalInvoiceAmount: '1500.00', + operationType: null, + }; + + it('should get details by access key (webhook v1)', async () => { + const mockResponse: HttpResponse = { + data: mockMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getDetails(testCompanyId, validAccessKey); + + expect(result).toEqual(mockMetadata); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}` + ); + }); + }); + + describe('getProductInvoiceDetails', () => { + const mockProductMetadata: InboundProductInvoiceMetadata = { + id: 'doc-123', + createdOn: '2024-01-15T10:30:00Z', + accessKey: validAccessKey, + parentAccessKey: '', + company: { id: testCompanyId, federalTaxNumber: '12345678000190' }, + issuer: { federalTaxNumber: '98765432000110', name: 'Issuer Corp' }, + buyer: { federalTaxNumber: '12345678000190', name: 'Buyer Corp' }, + transportation: { federalTaxNumber: '11111111000111', name: 'Transport Co' }, + links: { xml: 'https://example.com/xml', pdf: 'https://example.com/pdf' }, + xmlUrl: 'https://example.com/xml', + federalTaxNumberSender: '98765432000110', + nameSender: 'Issuer Corp', + type: null, + nsu: '12345', + nfeNumber: '1001', + issuedOn: '2024-01-10T08:00:00Z', + description: 'Test invoice', + totalInvoiceAmount: '1500.00', + productInvoices: [{ accessKey: '11111111111111111111111111111111111111111111' }], + }; + + it('should get product invoice details by access key (webhook v2)', async () => { + const mockResponse: HttpResponse = { + data: mockProductMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getProductInvoiceDetails(testCompanyId, validAccessKey); + + expect(result).toEqual(mockProductMetadata); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}` + ); + }); + }); + + // ========================================================================== + // getEventDetails() / getProductInvoiceEventDetails() tests + // ========================================================================== + + describe('getEventDetails', () => { + it('should get event details with correct path', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getEventDetails(testCompanyId, validAccessKey, testEventKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}` + ); + }); + + it('should throw ValidationError for empty eventKey', async () => { + await expect( + resource.getEventDetails(testCompanyId, validAccessKey, '') + ).rejects.toThrow(ValidationError); + }); + }); + + describe('getProductInvoiceEventDetails', () => { + it('should get product invoice event details with correct path', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundProductInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getProductInvoiceEventDetails(testCompanyId, validAccessKey, testEventKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}/events/${testEventKey}` + ); + }); + }); + + // ========================================================================== + // Download methods tests (getXml, getEventXml, getPdf, getJson) + // ========================================================================== + + describe('getXml', () => { + it('should download XML with correct path', async () => { + const mockResponse: HttpResponse = { + data: 'content', + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getXml(testCompanyId, validAccessKey); + + expect(result).toBe('content'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/xml` + ); + }); + }); + + describe('getEventXml', () => { + it('should download event XML with correct path', async () => { + const mockResponse: HttpResponse = { + data: 'event', + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getEventXml(testCompanyId, validAccessKey, testEventKey); + + expect(result).toBe('event'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}/xml` + ); + }); + + it('should throw ValidationError for empty eventKey', async () => { + await expect( + resource.getEventXml(testCompanyId, validAccessKey, '') + ).rejects.toThrow(ValidationError); + }); + }); + + describe('getPdf', () => { + it('should download PDF with correct path', async () => { + const mockResponse: HttpResponse = { + data: 'pdf-content', + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getPdf(testCompanyId, validAccessKey); + + expect(result).toBe('pdf-content'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/pdf` + ); + }); + }); + + describe('getJson', () => { + it('should get JSON with correct path', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getJson(testCompanyId, validAccessKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}/json` + ); + }); + }); + + // ========================================================================== + // manifest() tests + // ========================================================================== + + describe('manifest', () => { + it('should send manifest with default tpEvent=210210', async () => { + const mockResponse: HttpResponse = { + data: 'ok', + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const result = await resource.manifest(testCompanyId, validAccessKey); + + expect(result).toBe('ok'); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/manifest?tpEvent=210210` + ); + }); + + it('should send manifest with explicit tpEvent=210220', async () => { + const mockResponse: HttpResponse = { + data: 'ok', + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.manifest(testCompanyId, validAccessKey, 210220); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/manifest?tpEvent=210220` + ); + }); + + it('should send manifest with tpEvent=210240', async () => { + const mockResponse: HttpResponse = { + data: 'ok', + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.manifest(testCompanyId, validAccessKey, 210240); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/manifest?tpEvent=210240` + ); + }); + + it('should throw ValidationError for invalid access key', async () => { + await expect(resource.manifest(testCompanyId, 'invalid')).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // reprocessWebhook() tests + // ========================================================================== + + describe('reprocessWebhook', () => { + it('should reprocess webhook by access key', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundProductInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.reprocessWebhook(testCompanyId, validAccessKey); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}/processwebhook` + ); + }); + + it('should reprocess webhook by NSU', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundProductInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.reprocessWebhook(testCompanyId, '12345'); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/12345/processwebhook` + ); + }); + + it('should throw ValidationError for empty identifier', async () => { + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(/Access key or NSU is required/); + }); + + it('should throw ValidationError for whitespace-only identifier', async () => { + await expect(resource.reprocessWebhook(testCompanyId, ' ')).rejects.toThrow(ValidationError); + }); + }); +}); diff --git a/tests/unit/resources/product-invoice-query.test.ts b/tests/unit/resources/product-invoice-query.test.ts new file mode 100644 index 0000000..568d35c --- /dev/null +++ b/tests/unit/resources/product-invoice-query.test.ts @@ -0,0 +1,335 @@ +/** + * Unit tests for ProductInvoiceQueryResource + * Tests NF-e product invoice query operations (consulta-nf) + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ProductInvoiceQueryResource } from '../../../src/core/resources/product-invoice-query.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { + HttpResponse, + ProductInvoiceDetails, + ProductInvoiceEventsResponse, +} from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('ProductInvoiceQueryResource', () => { + let resource: ProductInvoiceQueryResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + getBuffer: ReturnType; + }; + + // Valid 44-digit access key for testing + const validAccessKey = '35240112345678000190550010000001231234567890'; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + getBuffer: vi.fn(), + }; + resource = new ProductInvoiceQueryResource(mockHttpClient as unknown as HttpClient); + }); + + // ========================================================================== + // Access Key Validation + // ========================================================================== + + describe('access key validation', () => { + it('should throw ValidationError for empty access key', async () => { + await expect(resource.retrieve('')).rejects.toThrow(ValidationError); + await expect(resource.retrieve('')).rejects.toThrow(/Access key is required/); + }); + + it('should throw ValidationError for whitespace-only access key', async () => { + await expect(resource.retrieve(' ')).rejects.toThrow(ValidationError); + await expect(resource.retrieve(' ')).rejects.toThrow(/Access key is required/); + }); + + it('should throw ValidationError for non-numeric access key', async () => { + await expect(resource.retrieve('a'.repeat(44))).rejects.toThrow(ValidationError); + await expect(resource.retrieve('a'.repeat(44))).rejects.toThrow(/Expected 44 numeric digits/); + }); + + it('should throw ValidationError for too-short access key', async () => { + await expect(resource.retrieve('12345')).rejects.toThrow(ValidationError); + await expect(resource.retrieve('12345')).rejects.toThrow(/Expected 44 numeric digits/); + }); + + it('should throw ValidationError for too-long access key', async () => { + await expect(resource.retrieve('1'.repeat(45))).rejects.toThrow(ValidationError); + }); + + it('should accept valid 44-digit numeric access key', async () => { + const mockResponse: HttpResponse = { + data: { accessKey: validAccessKey } as ProductInvoiceDetails, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await expect(resource.retrieve(validAccessKey)).resolves.toBeDefined(); + }); + + it('should validate access key on all methods', async () => { + const badKey = 'invalid'; + await expect(resource.retrieve(badKey)).rejects.toThrow(ValidationError); + await expect(resource.downloadPdf(badKey)).rejects.toThrow(ValidationError); + await expect(resource.downloadXml(badKey)).rejects.toThrow(ValidationError); + await expect(resource.listEvents(badKey)).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // retrieve() + // ========================================================================== + + describe('retrieve', () => { + it('should retrieve product invoice details by access key', async () => { + const mockInvoice: Partial = { + accessKey: validAccessKey, + currentStatus: 'authorized', + issuer: { + name: 'Test Company', + federalTaxNumber: '12345678000190', + } as any, + totals: { + icms: { + invoiceAmount: 1500.0, + }, + } as any, + }; + + const mockResponse: HttpResponse = { + data: mockInvoice as ProductInvoiceDetails, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.retrieve(validAccessKey); + + expect(result).toEqual(mockInvoice); + expect(result.accessKey).toBe(validAccessKey); + expect(result.currentStatus).toBe('authorized'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/productinvoices/${validAccessKey}` + ); + }); + + it('should call correct endpoint path', async () => { + const mockResponse: HttpResponse = { + data: {} as ProductInvoiceDetails, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.retrieve(validAccessKey); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/productinvoices/${validAccessKey}` + ); + }); + + it('should propagate HTTP errors from client', async () => { + const httpError = new Error('Not Found'); + (httpError as any).status = 404; + mockHttpClient.get.mockRejectedValue(httpError); + + await expect(resource.retrieve(validAccessKey)).rejects.toThrow('Not Found'); + }); + + it('should propagate authentication errors', async () => { + const authError = new Error('Unauthorized'); + (authError as any).status = 401; + mockHttpClient.get.mockRejectedValue(authError); + + await expect(resource.retrieve(validAccessKey)).rejects.toThrow('Unauthorized'); + }); + }); + + // ========================================================================== + // downloadPdf() + // ========================================================================== + + describe('downloadPdf', () => { + it('should download DANFE PDF as Buffer', async () => { + const pdfContent = Buffer.from('%PDF-1.4 mock content'); + const mockResponse: HttpResponse = { + data: pdfContent, + status: 200, + headers: {}, + }; + mockHttpClient.getBuffer.mockResolvedValue(mockResponse); + + const result = await resource.downloadPdf(validAccessKey); + + expect(result).toBeInstanceOf(Buffer); + expect(result).toEqual(pdfContent); + expect(mockHttpClient.getBuffer).toHaveBeenCalledWith( + `/v2/productinvoices/${validAccessKey}.pdf`, + 'application/pdf' + ); + }); + + it('should call correct endpoint with .pdf extension', async () => { + const mockResponse: HttpResponse = { + data: Buffer.from(''), + status: 200, + headers: {}, + }; + mockHttpClient.getBuffer.mockResolvedValue(mockResponse); + + await resource.downloadPdf(validAccessKey); + + expect(mockHttpClient.getBuffer).toHaveBeenCalledWith( + `/v2/productinvoices/${validAccessKey}.pdf`, + 'application/pdf' + ); + }); + + it('should propagate 404 errors', async () => { + const notFoundError = new Error('Not Found'); + (notFoundError as any).status = 404; + mockHttpClient.getBuffer.mockRejectedValue(notFoundError); + + await expect(resource.downloadPdf(validAccessKey)).rejects.toThrow('Not Found'); + }); + }); + + // ========================================================================== + // downloadXml() + // ========================================================================== + + describe('downloadXml', () => { + it('should download NF-e XML as Buffer', async () => { + const xmlContent = Buffer.from('mock'); + const mockResponse: HttpResponse = { + data: xmlContent, + status: 200, + headers: {}, + }; + mockHttpClient.getBuffer.mockResolvedValue(mockResponse); + + const result = await resource.downloadXml(validAccessKey); + + expect(result).toBeInstanceOf(Buffer); + expect(result).toEqual(xmlContent); + expect(mockHttpClient.getBuffer).toHaveBeenCalledWith( + `/v2/productinvoices/${validAccessKey}.xml`, + 'application/xml' + ); + }); + + it('should call correct endpoint with .xml extension', async () => { + const mockResponse: HttpResponse = { + data: Buffer.from(''), + status: 200, + headers: {}, + }; + mockHttpClient.getBuffer.mockResolvedValue(mockResponse); + + await resource.downloadXml(validAccessKey); + + expect(mockHttpClient.getBuffer).toHaveBeenCalledWith( + `/v2/productinvoices/${validAccessKey}.xml`, + 'application/xml' + ); + }); + + it('should propagate 404 errors', async () => { + const notFoundError = new Error('Not Found'); + (notFoundError as any).status = 404; + mockHttpClient.getBuffer.mockRejectedValue(notFoundError); + + await expect(resource.downloadXml(validAccessKey)).rejects.toThrow('Not Found'); + }); + }); + + // ========================================================================== + // listEvents() + // ========================================================================== + + describe('listEvents', () => { + it('should list fiscal events for an access key', async () => { + const mockEvents: ProductInvoiceEventsResponse = { + accessKey: validAccessKey, + events: [ + { + eventType: 'cancellation', + description: 'Cancelamento', + protocol: '135240112345678', + authorizedOn: '2024-03-15T10:30:00Z', + } as any, + ], + queriedAt: '2024-03-15T12:00:00Z', + }; + + const mockResponse: HttpResponse = { + data: mockEvents, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.listEvents(validAccessKey); + + expect(result.accessKey).toBe(validAccessKey); + expect(result.events).toHaveLength(1); + expect(result.events![0].eventType).toBe('cancellation'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/productinvoices/events/${validAccessKey}` + ); + }); + + it('should handle empty events array', async () => { + const mockEvents: ProductInvoiceEventsResponse = { + accessKey: validAccessKey, + events: [], + queriedAt: '2024-03-15T12:00:00Z', + }; + + const mockResponse: HttpResponse = { + data: mockEvents, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.listEvents(validAccessKey); + + expect(result.events).toEqual([]); + }); + + it('should call correct endpoint path with events segment', async () => { + const mockResponse: HttpResponse = { + data: { events: [] } as unknown as ProductInvoiceEventsResponse, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.listEvents(validAccessKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/productinvoices/events/${validAccessKey}` + ); + }); + + it('should propagate 404 errors', async () => { + const notFoundError = new Error('Not Found'); + (notFoundError as any).status = 404; + mockHttpClient.get.mockRejectedValue(notFoundError); + + await expect(resource.listEvents(validAccessKey)).rejects.toThrow('Not Found'); + }); + }); +}); diff --git a/tests/unit/resources/product-invoices.test.ts b/tests/unit/resources/product-invoices.test.ts new file mode 100644 index 0000000..f7e8ed9 --- /dev/null +++ b/tests/unit/resources/product-invoices.test.ts @@ -0,0 +1,560 @@ +/** + * Unit tests for ProductInvoicesResource + * Tests all CRUD, download, correction letter, and disablement operations. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ProductInvoicesResource } from '../../../src/core/resources/product-invoices.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { + HttpResponse, + NfeProductInvoiceIssueData, + NfeProductInvoice, + NfeProductInvoiceListResponse, + NfeInvoiceItemsResponse, + NfeProductInvoiceEventsResponse, + NfeFileResource, + NfeRequestCancellationResource, + NfeDisablementResource, +} from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('ProductInvoicesResource', () => { + let resource: ProductInvoicesResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + getBuffer: ReturnType; + }; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + getBuffer: vi.fn(), + }; + resource = new ProductInvoicesResource(mockHttpClient as unknown as HttpClient); + }); + + const companyId = 'company-123'; + const invoiceId = 'invoice-456'; + const stateTaxId = 'statetax-789'; + + function mockIssueData(): NfeProductInvoiceIssueData { + return { + operationNature: 'Venda de mercadoria', + operationType: 'Outgoing', + buyer: { + name: 'Empresa LTDA', + federalTaxNumber: 12345678000190, + }, + items: [ + { + code: 'PROD-001', + description: 'Produto X', + quantity: 1, + unitAmount: 100, + }, + ], + payment: [ + { + paymentDetail: [{ method: 'Cash', amount: 100 }], + }, + ], + } as NfeProductInvoiceIssueData; + } + + // -------------------------------------------------------------------------- + // create + // -------------------------------------------------------------------------- + + describe('create', () => { + it('should issue a product invoice', async () => { + const data = mockIssueData(); + const httpResponse: HttpResponse = { + data, + status: 202, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const result = await resource.create(companyId, data); + + expect(result).toEqual(data); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices`, + data, + ); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.create('', mockIssueData())).rejects.toThrow(ValidationError); + await expect(resource.create(' ', mockIssueData())).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // createWithStateTax + // -------------------------------------------------------------------------- + + describe('createWithStateTax', () => { + it('should issue via state tax endpoint', async () => { + const data = mockIssueData(); + const httpResponse: HttpResponse = { + data, + status: 202, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const result = await resource.createWithStateTax(companyId, stateTaxId, data); + + expect(result).toEqual(data); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes/${stateTaxId}/productinvoices`, + data, + ); + }); + + it('should throw ValidationError when stateTaxId is empty', async () => { + await expect(resource.createWithStateTax(companyId, '', mockIssueData())) + .rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.createWithStateTax('', stateTaxId, mockIssueData())) + .rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // list + // -------------------------------------------------------------------------- + + describe('list', () => { + const mockListResponse: NfeProductInvoiceListResponse = { + productInvoices: [], + hasMore: false, + totalCount: 0, + }; + + it('should list product invoices', async () => { + const httpResponse: HttpResponse = { + data: mockListResponse, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.list(companyId, { environment: 'Production' }); + + expect(result).toEqual(mockListResponse); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices`, + { environment: 'Production' }, + ); + }); + + it('should pass pagination params', async () => { + const httpResponse: HttpResponse = { + data: mockListResponse, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + await resource.list(companyId, { + environment: 'Test', + limit: 10, + startingAfter: 'cursor-abc', + }); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices`, + { environment: 'Test', limit: 10, startingAfter: 'cursor-abc' }, + ); + }); + + it('should throw ValidationError when environment is missing', async () => { + await expect(resource.list(companyId, {} as any)).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.list('', { environment: 'Production' })) + .rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // retrieve + // -------------------------------------------------------------------------- + + describe('retrieve', () => { + it('should retrieve a product invoice', async () => { + const mockInvoice = { id: invoiceId, status: 'Issued' } as unknown as NfeProductInvoice; + const httpResponse: HttpResponse = { + data: mockInvoice, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.retrieve(companyId, invoiceId); + + expect(result).toEqual(mockInvoice); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}`, + ); + }); + + it('should throw ValidationError when invoiceId is empty', async () => { + await expect(resource.retrieve(companyId, '')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.retrieve('', invoiceId)).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // cancel + // -------------------------------------------------------------------------- + + describe('cancel', () => { + const mockCancellation = { id: 'cancel-1' } as unknown as NfeRequestCancellationResource; + + it('should cancel without reason', async () => { + const httpResponse: HttpResponse = { + data: mockCancellation, + status: 204, + headers: {}, + }; + mockHttpClient.delete.mockResolvedValue(httpResponse); + + const result = await resource.cancel(companyId, invoiceId); + + expect(result).toEqual(mockCancellation); + expect(mockHttpClient.delete).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}`, + ); + }); + + it('should cancel with reason', async () => { + const httpResponse: HttpResponse = { + data: mockCancellation, + status: 204, + headers: {}, + }; + mockHttpClient.delete.mockResolvedValue(httpResponse); + + await resource.cancel(companyId, invoiceId, 'Erro nos dados'); + + expect(mockHttpClient.delete).toHaveBeenCalledWith( + expect.stringContaining('reason=Erro%20nos%20dados'), + ); + }); + + it('should throw ValidationError when invoiceId is empty', async () => { + await expect(resource.cancel(companyId, '')).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // listItems + // -------------------------------------------------------------------------- + + describe('listItems', () => { + it('should list invoice items', async () => { + const mockItems: NfeInvoiceItemsResponse = { items: [], hasMore: false, totalCount: 0 }; + const httpResponse: HttpResponse = { + data: mockItems, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.listItems(companyId, invoiceId); + + expect(result).toEqual(mockItems); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/items`, + {}, + ); + }); + + it('should pass pagination options', async () => { + const mockItems: NfeInvoiceItemsResponse = { items: [], hasMore: false, totalCount: 0 }; + mockHttpClient.get.mockResolvedValue({ data: mockItems, status: 200, headers: {} }); + + await resource.listItems(companyId, invoiceId, { limit: 5 }); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + expect.stringContaining('/items'), + { limit: 5 }, + ); + }); + + it('should throw ValidationError when invoiceId is empty', async () => { + await expect(resource.listItems(companyId, '')).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // listEvents + // -------------------------------------------------------------------------- + + describe('listEvents', () => { + it('should list invoice events', async () => { + const mockEvents: NfeProductInvoiceEventsResponse = { events: [], hasMore: false, totalCount: 0 }; + const httpResponse: HttpResponse = { + data: mockEvents, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.listEvents(companyId, invoiceId); + + expect(result).toEqual(mockEvents); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/events`, + {}, + ); + }); + }); + + // -------------------------------------------------------------------------- + // downloadPdf + // -------------------------------------------------------------------------- + + describe('downloadPdf', () => { + const mockFile: NfeFileResource = { uri: 'https://cdn.nfse.io/danfe.pdf' }; + + it('should get PDF file resource', async () => { + const httpResponse: HttpResponse = { + data: mockFile, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.downloadPdf(companyId, invoiceId); + + expect(result).toEqual(mockFile); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/pdf`, + {}, + ); + }); + + it('should pass force param', async () => { + mockHttpClient.get.mockResolvedValue({ data: mockFile, status: 200, headers: {} }); + + await resource.downloadPdf(companyId, invoiceId, true); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + expect.stringContaining('/pdf'), + { force: true }, + ); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.downloadPdf('', invoiceId)).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // downloadXml + // -------------------------------------------------------------------------- + + describe('downloadXml', () => { + it('should get XML file resource', async () => { + const mockFile: NfeFileResource = { uri: 'https://cdn.nfse.io/nfe.xml' }; + mockHttpClient.get.mockResolvedValue({ data: mockFile, status: 200, headers: {} }); + + const result = await resource.downloadXml(companyId, invoiceId); + + expect(result).toEqual(mockFile); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/xml`, + ); + }); + }); + + // -------------------------------------------------------------------------- + // downloadRejectionXml + // -------------------------------------------------------------------------- + + describe('downloadRejectionXml', () => { + it('should use /xml-rejection canonical path', async () => { + const mockFile: NfeFileResource = { uri: 'https://cdn.nfse.io/rejection.xml' }; + mockHttpClient.get.mockResolvedValue({ data: mockFile, status: 200, headers: {} }); + + const result = await resource.downloadRejectionXml(companyId, invoiceId); + + expect(result).toEqual(mockFile); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/xml-rejection`, + ); + }); + }); + + // -------------------------------------------------------------------------- + // downloadEpecXml + // -------------------------------------------------------------------------- + + describe('downloadEpecXml', () => { + it('should get EPEC XML file resource', async () => { + const mockFile: NfeFileResource = { uri: 'https://cdn.nfse.io/epec.xml' }; + mockHttpClient.get.mockResolvedValue({ data: mockFile, status: 200, headers: {} }); + + const result = await resource.downloadEpecXml(companyId, invoiceId); + + expect(result).toEqual(mockFile); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/xml-epec`, + ); + }); + }); + + // -------------------------------------------------------------------------- + // sendCorrectionLetter + // -------------------------------------------------------------------------- + + describe('sendCorrectionLetter', () => { + const mockResult = { id: 'cc-1' } as unknown as NfeRequestCancellationResource; + const validReason = 'Correcao no endereco do destinatario conforme informado pelo cliente'; + + it('should send correction letter', async () => { + mockHttpClient.put.mockResolvedValue({ data: mockResult, status: 200, headers: {} }); + + const result = await resource.sendCorrectionLetter(companyId, invoiceId, validReason); + + expect(result).toEqual(mockResult); + expect(mockHttpClient.put).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/correctionletter`, + { reason: validReason }, + ); + }); + + it('should throw if reason is too short', async () => { + await expect(resource.sendCorrectionLetter(companyId, invoiceId, 'short')) + .rejects.toThrow(ValidationError); + await expect(resource.sendCorrectionLetter(companyId, invoiceId, 'short')) + .rejects.toThrow(/at least 15 characters/); + }); + + it('should throw if reason exceeds 1000 characters', async () => { + const longReason = 'x'.repeat(1001); + await expect(resource.sendCorrectionLetter(companyId, invoiceId, longReason)) + .rejects.toThrow(ValidationError); + await expect(resource.sendCorrectionLetter(companyId, invoiceId, longReason)) + .rejects.toThrow(/at most 1,000 characters/); + }); + + it('should throw if reason is empty', async () => { + await expect(resource.sendCorrectionLetter(companyId, invoiceId, '')) + .rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // downloadCorrectionLetterPdf / downloadCorrectionLetterXml + // -------------------------------------------------------------------------- + + describe('downloadCorrectionLetterPdf', () => { + it('should get CC-e PDF', async () => { + const mockFile: NfeFileResource = { uri: 'https://cdn.nfse.io/cce.pdf' }; + mockHttpClient.get.mockResolvedValue({ data: mockFile, status: 200, headers: {} }); + + const result = await resource.downloadCorrectionLetterPdf(companyId, invoiceId); + + expect(result).toEqual(mockFile); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/correctionletter/pdf`, + ); + }); + }); + + describe('downloadCorrectionLetterXml', () => { + it('should get CC-e XML', async () => { + const mockFile: NfeFileResource = { uri: 'https://cdn.nfse.io/cce.xml' }; + mockHttpClient.get.mockResolvedValue({ data: mockFile, status: 200, headers: {} }); + + const result = await resource.downloadCorrectionLetterXml(companyId, invoiceId); + + expect(result).toEqual(mockFile); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/correctionletter/xml`, + ); + }); + }); + + // -------------------------------------------------------------------------- + // disable + // -------------------------------------------------------------------------- + + describe('disable', () => { + const mockResult = { id: 'dis-1' } as unknown as NfeRequestCancellationResource; + + it('should disable without reason', async () => { + mockHttpClient.post.mockResolvedValue({ data: mockResult, status: 204, headers: {} }); + + const result = await resource.disable(companyId, invoiceId); + + expect(result).toEqual(mockResult); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/${invoiceId}/disablement`, + ); + }); + + it('should disable with reason query param', async () => { + mockHttpClient.post.mockResolvedValue({ data: mockResult, status: 204, headers: {} }); + + await resource.disable(companyId, invoiceId, 'Numero inutilizado'); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + expect.stringContaining('reason=Numero%20inutilizado'), + ); + }); + + it('should throw when invoiceId is empty', async () => { + await expect(resource.disable(companyId, '')).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // disableRange + // -------------------------------------------------------------------------- + + describe('disableRange', () => { + it('should disable a range of invoice numbers', async () => { + const data = { + environment: 'Production' as const, + serie: 1, + state: 'SP', + beginNumber: 100, + lastNumber: 110, + reason: 'Erro de sequencia', + }; + const mockResult = { id: 'dis-range-1' } as unknown as NfeDisablementResource; + mockHttpClient.post.mockResolvedValue({ data: mockResult, status: 200, headers: {} }); + + const result = await resource.disableRange(companyId, data); + + expect(result).toEqual(mockResult); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${companyId}/productinvoices/disablement`, + data, + ); + }); + + it('should throw when companyId is empty', async () => { + await expect(resource.disableRange('', {} as any)).rejects.toThrow(ValidationError); + }); + }); +}); diff --git a/tests/unit/resources/state-taxes.test.ts b/tests/unit/resources/state-taxes.test.ts new file mode 100644 index 0000000..ee1bc51 --- /dev/null +++ b/tests/unit/resources/state-taxes.test.ts @@ -0,0 +1,225 @@ +/** + * Unit tests for StateTaxesResource + * Tests CRUD operations, validation, and API error handling. + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { StateTaxesResource } from '../../../src/core/resources/state-taxes.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { + HttpResponse, + NfeStateTax, + NfeStateTaxCreateData, + NfeStateTaxUpdateData, + NfeStateTaxListResponse, +} from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('StateTaxesResource', () => { + let resource: StateTaxesResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + }; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + resource = new StateTaxesResource(mockHttpClient as unknown as HttpClient); + }); + + const companyId = 'company-123'; + const stateTaxId = 'statetax-456'; + + const mockStateTax: NfeStateTax = { + id: stateTaxId, + taxNumber: '123456789', + serie: 1, + lastNumber: 0, + code: 'sP', + environmentType: 'production', + status: 'active', + type: 'nFe', + } as NfeStateTax; + + // -------------------------------------------------------------------------- + // list + // -------------------------------------------------------------------------- + + describe('list', () => { + const mockListResponse: NfeStateTaxListResponse = { + stateTaxes: [mockStateTax], + hasMore: false, + totalCount: 1, + }; + + it('should list state taxes', async () => { + const httpResponse: HttpResponse = { + data: mockListResponse, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.list(companyId); + + expect(result).toEqual(mockListResponse); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes`, + {}, + ); + }); + + it('should pass pagination options', async () => { + mockHttpClient.get.mockResolvedValue({ + data: mockListResponse, + status: 200, + headers: {}, + }); + + await resource.list(companyId, { limit: 5, startingAfter: 'cursor-abc' }); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes`, + { limit: 5, startingAfter: 'cursor-abc' }, + ); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.list('')).rejects.toThrow(ValidationError); + await expect(resource.list(' ')).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // create + // -------------------------------------------------------------------------- + + describe('create', () => { + const createData: NfeStateTaxCreateData = { + taxNumber: '123456789', + serie: 1, + number: 1, + code: 'sP', + environmentType: 'production', + } as NfeStateTaxCreateData; + + it('should create a state tax registration', async () => { + const httpResponse: HttpResponse = { + data: mockStateTax, + status: 201, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const result = await resource.create(companyId, createData); + + expect(result).toEqual(mockStateTax); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes`, + { stateTax: createData }, + ); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.create('', createData)).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // retrieve + // -------------------------------------------------------------------------- + + describe('retrieve', () => { + it('should retrieve a state tax by ID', async () => { + const httpResponse: HttpResponse = { + data: mockStateTax, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + + const result = await resource.retrieve(companyId, stateTaxId); + + expect(result).toEqual(mockStateTax); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes/${stateTaxId}`, + ); + }); + + it('should throw ValidationError when stateTaxId is empty', async () => { + await expect(resource.retrieve(companyId, '')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.retrieve('', stateTaxId)).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // update + // -------------------------------------------------------------------------- + + describe('update', () => { + const updateData: NfeStateTaxUpdateData = { + serie: 2, + environmentType: 'test', + } as NfeStateTaxUpdateData; + + it('should update a state tax registration', async () => { + const updatedTax = { ...mockStateTax, serie: 2, environmentType: 'test' as const }; + const httpResponse: HttpResponse = { + data: updatedTax, + status: 200, + headers: {}, + }; + mockHttpClient.put.mockResolvedValue(httpResponse); + + const result = await resource.update(companyId, stateTaxId, updateData); + + expect(result).toEqual(updatedTax); + expect(mockHttpClient.put).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes/${stateTaxId}`, + { stateTax: updateData }, + ); + }); + + it('should throw ValidationError when stateTaxId is empty', async () => { + await expect(resource.update(companyId, '', updateData)).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.update('', stateTaxId, updateData)).rejects.toThrow(ValidationError); + }); + }); + + // -------------------------------------------------------------------------- + // delete + // -------------------------------------------------------------------------- + + describe('delete', () => { + it('should delete a state tax registration', async () => { + mockHttpClient.delete.mockResolvedValue({ data: undefined, status: 204, headers: {} }); + + await resource.delete(companyId, stateTaxId); + + expect(mockHttpClient.delete).toHaveBeenCalledWith( + `/v2/companies/${companyId}/statetaxes/${stateTaxId}`, + ); + }); + + it('should throw ValidationError when stateTaxId is empty', async () => { + await expect(resource.delete(companyId, '')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError when companyId is empty', async () => { + await expect(resource.delete('', stateTaxId)).rejects.toThrow(ValidationError); + }); + }); +}); diff --git a/tests/unit/resources/tax-calculation.test.ts b/tests/unit/resources/tax-calculation.test.ts new file mode 100644 index 0000000..52eaeed --- /dev/null +++ b/tests/unit/resources/tax-calculation.test.ts @@ -0,0 +1,176 @@ +/** + * Unit tests for TaxCalculationResource + * Tests calculate() success, validation errors, and API error handling + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { TaxCalculationResource } from '../../../src/core/resources/tax-calculation.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { HttpResponse, CalculateRequest, CalculateResponse } from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('TaxCalculationResource', () => { + let resource: TaxCalculationResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + }; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + resource = new TaxCalculationResource(mockHttpClient as unknown as HttpClient); + }); + + // Helper to build a valid request + function validRequest(): CalculateRequest { + return { + operationType: 'Outgoing', + issuer: { state: 'SP', taxRegime: 'RealProfit' }, + recipient: { state: 'RJ' }, + items: [ + { + id: 'item-1', + operationCode: 121, + origin: 'National', + quantity: 10, + unitAmount: 100.0, + }, + ], + }; + } + + describe('calculate', () => { + const mockResponse: CalculateResponse = { + items: [ + { + id: 'item-1', + cfop: 6102, + icms: { cst: '00', vBC: '1000.00', pICMS: '12.00', vICMS: '120.00' }, + pis: { cst: '01', vBC: '1000.00', pPIS: '1.65', vPIS: '16.50' }, + cofins: { cst: '01', vBC: '1000.00', pCOFINS: '7.60', vCOFINS: '76.00' }, + }, + ], + }; + + it('should calculate taxes successfully', async () => { + const httpResponse: HttpResponse = { + data: mockResponse, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + const result = await resource.calculate('tenant-123', validRequest()); + + expect(result).toEqual(mockResponse); + expect(result.items).toHaveLength(1); + expect(result.items![0]!.cfop).toBe(6102); + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/tax-rules/tenant-123/engine/calculate', + validRequest() + ); + }); + + it('should encode tenantId in URL', async () => { + const httpResponse: HttpResponse = { + data: mockResponse, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + await resource.calculate('tenant with spaces', validRequest()); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/tax-rules/tenant%20with%20spaces/engine/calculate', + validRequest() + ); + }); + + it('should trim tenantId', async () => { + const httpResponse: HttpResponse = { + data: mockResponse, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(httpResponse); + + await resource.calculate(' tenant-123 ', validRequest()); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/tax-rules/tenant-123/engine/calculate', + validRequest() + ); + }); + + // --- tenantId validation --- + + it('should throw ValidationError for empty tenantId', async () => { + await expect(resource.calculate('', validRequest())).rejects.toThrow(ValidationError); + await expect(resource.calculate('', validRequest())).rejects.toThrow(/tenantId is required/); + }); + + it('should throw ValidationError for whitespace-only tenantId', async () => { + await expect(resource.calculate(' ', validRequest())).rejects.toThrow(ValidationError); + }); + + // --- CalculateRequest validation --- + + it('should throw ValidationError when request is null/undefined', async () => { + await expect( + resource.calculate('tenant-123', null as unknown as CalculateRequest) + ).rejects.toThrow(ValidationError); + await expect( + resource.calculate('tenant-123', null as unknown as CalculateRequest) + ).rejects.toThrow(/request is required/); + }); + + it('should throw ValidationError when issuer is missing', async () => { + const request = { ...validRequest(), issuer: undefined } as unknown as CalculateRequest; + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(ValidationError); + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(/issuer is required/); + }); + + it('should throw ValidationError when recipient is missing', async () => { + const request = { ...validRequest(), recipient: undefined } as unknown as CalculateRequest; + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(ValidationError); + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(/recipient is required/); + }); + + it('should throw ValidationError when operationType is missing', async () => { + const request = { ...validRequest(), operationType: '' } as unknown as CalculateRequest; + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(ValidationError); + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(/operationType is required/); + }); + + it('should throw ValidationError when items is empty', async () => { + const request = { ...validRequest(), items: [] }; + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(ValidationError); + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(/items is required/); + }); + + it('should throw ValidationError when items is not an array', async () => { + const request = { ...validRequest(), items: 'not-array' } as unknown as CalculateRequest; + await expect(resource.calculate('tenant-123', request)).rejects.toThrow(ValidationError); + }); + + // --- API error handling --- + + it('should propagate API errors', async () => { + mockHttpClient.post.mockRejectedValue(new Error('Bad Request')); + await expect(resource.calculate('tenant-123', validRequest())).rejects.toThrow('Bad Request'); + }); + + it('should propagate network errors', async () => { + mockHttpClient.post.mockRejectedValue(new Error('Network error')); + await expect(resource.calculate('tenant-123', validRequest())).rejects.toThrow('Network error'); + }); + }); +}); diff --git a/tests/unit/resources/tax-codes.test.ts b/tests/unit/resources/tax-codes.test.ts new file mode 100644 index 0000000..e86acbe --- /dev/null +++ b/tests/unit/resources/tax-codes.test.ts @@ -0,0 +1,196 @@ +/** + * Unit tests for TaxCodesResource + * Tests all four list methods, pagination parameter passing, and default behavior + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { TaxCodesResource } from '../../../src/core/resources/tax-codes.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { HttpResponse, TaxCodePaginatedResponse } from '../../../src/core/types.js'; + +describe('TaxCodesResource', () => { + let resource: TaxCodesResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + }; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + resource = new TaxCodesResource(mockHttpClient as unknown as HttpClient); + }); + + const mockPaginatedResponse: TaxCodePaginatedResponse = { + items: [ + { code: '121', description: 'Venda de mercadoria' }, + { code: '122', description: 'Venda de mercadoria para entrega futura' }, + ], + currentPage: 1, + totalPages: 5, + totalCount: 100, + }; + + function mockGet(): void { + const httpResponse: HttpResponse = { + data: mockPaginatedResponse, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(httpResponse); + } + + // ============================================================================ + // listOperationCodes + // ============================================================================ + + describe('listOperationCodes', () => { + it('should list operation codes without options', async () => { + mockGet(); + + const result = await resource.listOperationCodes(); + + expect(result).toEqual(mockPaginatedResponse); + expect(result.items).toHaveLength(2); + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/operation-code'); + }); + + it('should pass pageIndex parameter', async () => { + mockGet(); + + await resource.listOperationCodes({ pageIndex: 3 }); + + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/operation-code?pageIndex=3'); + }); + + it('should pass pageCount parameter', async () => { + mockGet(); + + await resource.listOperationCodes({ pageCount: 20 }); + + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/operation-code?pageCount=20'); + }); + + it('should pass both pagination parameters', async () => { + mockGet(); + + await resource.listOperationCodes({ pageIndex: 2, pageCount: 10 }); + + const calledUrl = mockHttpClient.get.mock.calls[0]![0] as string; + expect(calledUrl).toContain('pageIndex=2'); + expect(calledUrl).toContain('pageCount=10'); + }); + + it('should skip undefined pagination values', async () => { + mockGet(); + + await resource.listOperationCodes({}); + + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/operation-code'); + }); + + it('should propagate API errors', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Unauthorized')); + await expect(resource.listOperationCodes()).rejects.toThrow('Unauthorized'); + }); + }); + + // ============================================================================ + // listAcquisitionPurposes + // ============================================================================ + + describe('listAcquisitionPurposes', () => { + it('should list acquisition purposes without options', async () => { + mockGet(); + + const result = await resource.listAcquisitionPurposes(); + + expect(result).toEqual(mockPaginatedResponse); + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/acquisition-purpose'); + }); + + it('should pass pagination parameters', async () => { + mockGet(); + + await resource.listAcquisitionPurposes({ pageIndex: 2, pageCount: 25 }); + + const calledUrl = mockHttpClient.get.mock.calls[0]![0] as string; + expect(calledUrl).toContain('/tax-codes/acquisition-purpose'); + expect(calledUrl).toContain('pageIndex=2'); + expect(calledUrl).toContain('pageCount=25'); + }); + + it('should propagate API errors', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Not Found')); + await expect(resource.listAcquisitionPurposes()).rejects.toThrow('Not Found'); + }); + }); + + // ============================================================================ + // listIssuerTaxProfiles + // ============================================================================ + + describe('listIssuerTaxProfiles', () => { + it('should list issuer tax profiles without options', async () => { + mockGet(); + + const result = await resource.listIssuerTaxProfiles(); + + expect(result).toEqual(mockPaginatedResponse); + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/issuer-tax-profile'); + }); + + it('should pass pagination parameters', async () => { + mockGet(); + + await resource.listIssuerTaxProfiles({ pageIndex: 1, pageCount: 50 }); + + const calledUrl = mockHttpClient.get.mock.calls[0]![0] as string; + expect(calledUrl).toContain('/tax-codes/issuer-tax-profile'); + expect(calledUrl).toContain('pageIndex=1'); + expect(calledUrl).toContain('pageCount=50'); + }); + + it('should propagate API errors', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Server Error')); + await expect(resource.listIssuerTaxProfiles()).rejects.toThrow('Server Error'); + }); + }); + + // ============================================================================ + // listRecipientTaxProfiles + // ============================================================================ + + describe('listRecipientTaxProfiles', () => { + it('should list recipient tax profiles without options', async () => { + mockGet(); + + const result = await resource.listRecipientTaxProfiles(); + + expect(result).toEqual(mockPaginatedResponse); + expect(mockHttpClient.get).toHaveBeenCalledWith('/tax-codes/recipient-tax-profile'); + }); + + it('should pass pagination parameters', async () => { + mockGet(); + + await resource.listRecipientTaxProfiles({ pageIndex: 3, pageCount: 15 }); + + const calledUrl = mockHttpClient.get.mock.calls[0]![0] as string; + expect(calledUrl).toContain('/tax-codes/recipient-tax-profile'); + expect(calledUrl).toContain('pageIndex=3'); + expect(calledUrl).toContain('pageCount=15'); + }); + + it('should propagate API errors', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Timeout')); + await expect(resource.listRecipientTaxProfiles()).rejects.toThrow('Timeout'); + }); + }); +}); diff --git a/tests/unit/resources/tax-types.test.ts b/tests/unit/resources/tax-types.test.ts new file mode 100644 index 0000000..ef4a7a7 --- /dev/null +++ b/tests/unit/resources/tax-types.test.ts @@ -0,0 +1,231 @@ +/** + * Type-level tests for tax calculation types. + * + * These tests verify required/optional fields on request/response interfaces + * at compile time. If any assertion fails, TypeScript will produce a compile error. + */ + +import { describe, it, expect } from 'vitest'; +import type { + TaxOperationType, + TaxOrigin, + TaxCalcTaxRegime, + BrazilianState, + TaxIcms, + TaxIcmsUfDest, + TaxPis, + TaxCofins, + TaxIpi, + TaxIi, + CalculateRequestIssuer, + CalculateRequestRecipient, + CalculateItemRequest, + CalculateRequest, + CalculateItemResponse, + CalculateResponse, + TaxCode, + TaxCodePaginatedResponse, + TaxCodeListOptions, +} from '../../../src/core/types.js'; + +// ============================================================================ +// Compile-time type assertion helpers +// ============================================================================ + +/** + * Assert that type A is assignable to type B. + * If not, TypeScript will produce a compile error. + */ +type AssertAssignable<_A extends B, B> = true; + +// ============================================================================ +// Enum / union type checks +// ============================================================================ + +// TaxOperationType accepts only valid values +type _OpOutgoing = AssertAssignable<'Outgoing', TaxOperationType>; +type _OpIncoming = AssertAssignable<'Incoming', TaxOperationType>; + +// TaxOrigin accepts all 9 values +type _OriginNational = AssertAssignable<'National', TaxOrigin>; +type _OriginFDI = AssertAssignable<'ForeignDirectImport', TaxOrigin>; +type _OriginFIM = AssertAssignable<'ForeignInternalMarket', TaxOrigin>; + +// TaxCalcTaxRegime accepts all 6 values +type _RegimeNS = AssertAssignable<'NationalSimple', TaxCalcTaxRegime>; +type _RegimeRP = AssertAssignable<'RealProfit', TaxCalcTaxRegime>; +type _RegimePP = AssertAssignable<'PresumedProfit', TaxCalcTaxRegime>; +type _RegimeExempt = AssertAssignable<'Exempt', TaxCalcTaxRegime>; + +// BrazilianState accepts known values +type _StateSP = AssertAssignable<'SP', BrazilianState>; +type _StateEX = AssertAssignable<'EX', BrazilianState>; + +// ============================================================================ +// Runtime tests verifying type structure +// ============================================================================ + +describe('Tax Calculation Type-level Tests', () => { + it('CalculateRequest requires issuer, recipient, operationType, items', () => { + // Valid: all required fields + const request: CalculateRequest = { + issuer: { state: 'SP', taxRegime: 'RealProfit' }, + recipient: { state: 'RJ' }, + operationType: 'Outgoing', + items: [{ id: '1', operationCode: 121, origin: 'National', quantity: 1, unitAmount: 100 }], + }; + expect(request.issuer).toBeDefined(); + expect(request.recipient).toBeDefined(); + expect(request.operationType).toBeDefined(); + expect(request.items).toHaveLength(1); + + // Optional fields should be assignable + const requestWithOptionals: CalculateRequest = { + ...request, + collectionId: 'col-1', + isProductRegistration: false, + }; + expect(requestWithOptionals.collectionId).toBe('col-1'); + }); + + it('CalculateRequestIssuer requires state and taxRegime', () => { + const issuer: CalculateRequestIssuer = { + state: 'SP', + taxRegime: 'RealProfit', + }; + expect(issuer.state).toBe('SP'); + expect(issuer.taxRegime).toBe('RealProfit'); + + // Optional field + const withProfile: CalculateRequestIssuer = { + ...issuer, + taxProfile: 'industry', + }; + expect(withProfile.taxProfile).toBe('industry'); + }); + + it('CalculateRequestRecipient requires only state', () => { + // Minimal: only state is required + const recipient: CalculateRequestRecipient = { state: 'RJ' }; + expect(recipient.state).toBe('RJ'); + + // All fields + const full: CalculateRequestRecipient = { + state: 'MG', + taxRegime: 'NationalSimple', + taxProfile: 'final_consumer_non_icms_contributor', + }; + expect(full.taxRegime).toBe('NationalSimple'); + }); + + it('CalculateItemRequest requires id, operationCode, origin, quantity, unitAmount', () => { + const item: CalculateItemRequest = { + id: 'item-1', + operationCode: 121, + origin: 'National', + quantity: 10, + unitAmount: 50.0, + }; + expect(item.id).toBe('item-1'); + expect(item.operationCode).toBe(121); + + // All optional fields + const fullItem: CalculateItemRequest = { + ...item, + acquisitionPurpose: '569', + issuerTaxProfile: 'industry', + recipientTaxProfile: 'industry', + sku: 'SKU-001', + ncm: '61091000', + cest: '1234567', + benefit: 'BEN01', + exTipi: '01', + gtin: '7891234567890', + freightAmount: 10, + insuranceAmount: 5, + discountAmount: 2, + othersAmount: 1, + }; + expect(fullItem.ncm).toBe('61091000'); + }); + + it('CalculateItemResponse has all optional fields', () => { + // Empty response is valid (all fields optional) + const empty: CalculateItemResponse = {}; + expect(empty.id).toBeUndefined(); + + // Full response + const full: CalculateItemResponse = { + id: 'item-1', + cfop: 6102, + cest: '1234567', + benefit: 'BEN01', + icms: { cst: '00', vBC: '1000.00' }, + icmsUfDest: { vBCUFDest: '1000.00' }, + pis: { cst: '01' }, + cofins: { cst: '01' }, + ipi: { cst: '50' }, + ii: { vBC: '0.00' }, + additionalInformation: 'test', + lastModified: '2025-01-01T00:00:00Z', + productId: 'prod-1', + }; + expect(full.cfop).toBe(6102); + }); + + it('CalculateResponse has optional items array', () => { + const empty: CalculateResponse = {}; + expect(empty.items).toBeUndefined(); + + const withItems: CalculateResponse = { items: [{ id: '1', cfop: 5102 }] }; + expect(withItems.items).toHaveLength(1); + }); + + it('TaxCode has all optional string fields', () => { + const code: TaxCode = {}; + expect(code.code).toBeUndefined(); + + const full: TaxCode = { code: '121', description: 'Venda de mercadoria' }; + expect(full.code).toBe('121'); + }); + + it('TaxCodePaginatedResponse has correct shape', () => { + const response: TaxCodePaginatedResponse = { + items: [{ code: '121', description: 'test' }], + currentPage: 1, + totalPages: 5, + totalCount: 100, + }; + expect(response.items).toHaveLength(1); + expect(response.currentPage).toBe(1); + expect(response.totalPages).toBe(5); + expect(response.totalCount).toBe(100); + }); + + it('TaxCodeListOptions has all optional pagination fields', () => { + // Empty is valid + const empty: TaxCodeListOptions = {}; + expect(empty.pageIndex).toBeUndefined(); + + // Full + const full: TaxCodeListOptions = { pageIndex: 2, pageCount: 25 }; + expect(full.pageIndex).toBe(2); + }); + + it('Tax component interfaces have all optional string fields', () => { + // All tax components should work with empty objects + const icms: TaxIcms = {}; + const icmsUfDest: TaxIcmsUfDest = {}; + const pis: TaxPis = {}; + const cofins: TaxCofins = {}; + const ipi: TaxIpi = {}; + const ii: TaxIi = {}; + + expect(icms.cst).toBeUndefined(); + expect(icmsUfDest.vBCUFDest).toBeUndefined(); + expect(pis.cst).toBeUndefined(); + expect(cofins.cst).toBeUndefined(); + expect(ipi.cst).toBeUndefined(); + expect(ii.vBC).toBeUndefined(); + }); +}); diff --git a/tests/unit/resources/transportation-invoices.test.ts b/tests/unit/resources/transportation-invoices.test.ts new file mode 100644 index 0000000..98e8e4b --- /dev/null +++ b/tests/unit/resources/transportation-invoices.test.ts @@ -0,0 +1,487 @@ +/** + * Unit tests for TransportationInvoicesResource + * Tests CT-e (Conhecimento de Transporte Eletrônico) operations + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { TransportationInvoicesResource } from '../../../src/core/resources/transportation-invoices.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { + HttpResponse, + TransportationInvoiceInboundSettings, + TransportationInvoiceMetadata +} from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('TransportationInvoicesResource', () => { + let resource: TransportationInvoicesResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + }; + + // Valid 44-digit access key for testing + const validAccessKey = '35240112345678000190570010000001231234567890'; + const testCompanyId = 'company-123'; + const testEventKey = 'event-key-456'; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + resource = new TransportationInvoicesResource(mockHttpClient as unknown as HttpClient); + }); + + // ========================================================================== + // enable() tests + // ========================================================================== + + describe('enable', () => { + const mockSettings: TransportationInvoiceInboundSettings = { + status: 'Active', + startFromNsu: 1, + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-15T10:30:00Z', + }; + + it('should enable automatic CT-e search with default options', async () => { + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const result = await resource.enable(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/transportationinvoices`, + {} + ); + }); + + it('should enable automatic CT-e search with startFromNsu option', async () => { + const mockResponse: HttpResponse = { + data: { ...mockSettings, startFromNsu: 12345 }, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const result = await resource.enable(testCompanyId, { startFromNsu: 12345 }); + + expect(result.startFromNsu).toBe(12345); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/transportationinvoices`, + { startFromNsu: 12345 } + ); + }); + + it('should enable automatic CT-e search with startFromDate option', async () => { + const startDate = '2024-01-01T00:00:00Z'; + const mockResponse: HttpResponse = { + data: { ...mockSettings, startFromDate: startDate }, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const result = await resource.enable(testCompanyId, { startFromDate: startDate }); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/transportationinvoices`, + { startFromDate: startDate } + ); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.enable('')).rejects.toThrow(ValidationError); + await expect(resource.enable('')).rejects.toThrow(/Company ID is required/); + }); + + it('should throw ValidationError for whitespace-only company ID', async () => { + await expect(resource.enable(' ')).rejects.toThrow(ValidationError); + }); + + it('should handle API error responses', async () => { + mockHttpClient.post.mockRejectedValue(new Error('Company not found')); + + await expect(resource.enable('invalid-company')).rejects.toThrow('Company not found'); + }); + }); + + // ========================================================================== + // disable() tests + // ========================================================================== + + describe('disable', () => { + const mockSettings: TransportationInvoiceInboundSettings = { + status: 'Disabled', + startFromNsu: 1, + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-16T08:00:00Z', + }; + + it('should disable automatic CT-e search', async () => { + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.delete.mockResolvedValue(mockResponse); + + const result = await resource.disable(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.delete).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/transportationinvoices` + ); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.disable('')).rejects.toThrow(ValidationError); + await expect(resource.disable('')).rejects.toThrow(/Company ID is required/); + }); + + it('should handle API error responses', async () => { + mockHttpClient.delete.mockRejectedValue(new Error('Not enabled')); + + await expect(resource.disable('invalid-company')).rejects.toThrow('Not enabled'); + }); + }); + + // ========================================================================== + // getSettings() tests + // ========================================================================== + + describe('getSettings', () => { + const mockSettings: TransportationInvoiceInboundSettings = { + status: 'Active', + startFromNsu: 5000, + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-15T10:30:00Z', + }; + + it('should retrieve current CT-e search settings', async () => { + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getSettings(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/transportationinvoices` + ); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.getSettings('')).rejects.toThrow(ValidationError); + await expect(resource.getSettings('')).rejects.toThrow(/Company ID is required/); + }); + + it('should handle API error responses', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Not found')); + + await expect(resource.getSettings('invalid-company')).rejects.toThrow('Not found'); + }); + }); + + // ========================================================================== + // retrieve() tests + // ========================================================================== + + describe('retrieve', () => { + const mockMetadata: TransportationInvoiceMetadata = { + accessKey: validAccessKey, + type: 'TransportationInvoice', + nameSender: 'Test Sender Company', + federalTaxNumberSender: '12345678000190', + totalInvoiceAmount: 1500.50, + issuedOn: '2024-01-15T14:30:00Z', + receivedOn: '2024-01-15T15:00:00Z', + status: 'Authorized', + }; + + it('should retrieve CT-e metadata by access key', async () => { + const mockResponse: HttpResponse = { + data: mockMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.retrieve(testCompanyId, validAccessKey); + + expect(result).toEqual(mockMetadata); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}` + ); + }); + + it('should handle access key with whitespace', async () => { + const mockResponse: HttpResponse = { + data: mockMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.retrieve(testCompanyId, ` ${validAccessKey} `); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}` + ); + }); + + it('should throw ValidationError for empty access key', async () => { + await expect(resource.retrieve(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.retrieve(testCompanyId, '')).rejects.toThrow(/Access key is required/); + }); + + it('should throw ValidationError for whitespace-only access key', async () => { + await expect(resource.retrieve(testCompanyId, ' ')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for access key with less than 44 digits', async () => { + await expect(resource.retrieve(testCompanyId, '123456789012345678901234567890123456789012')).rejects.toThrow(ValidationError); + await expect(resource.retrieve(testCompanyId, '12345')).rejects.toThrow(/Invalid access key/); + }); + + it('should throw ValidationError for access key with more than 44 digits', async () => { + const longKey = '123456789012345678901234567890123456789012345'; + await expect(resource.retrieve(testCompanyId, longKey)).rejects.toThrow(ValidationError); + await expect(resource.retrieve(testCompanyId, longKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should throw ValidationError for access key with non-numeric characters', async () => { + const invalidKey = '3524011234567800019057001000000123123456789a'; + await expect(resource.retrieve(testCompanyId, invalidKey)).rejects.toThrow(ValidationError); + await expect(resource.retrieve(testCompanyId, invalidKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should throw ValidationError for access key with letters', async () => { + const invalidKey = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP'; + await expect(resource.retrieve(testCompanyId, invalidKey)).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.retrieve('', validAccessKey)).rejects.toThrow(ValidationError); + await expect(resource.retrieve('', validAccessKey)).rejects.toThrow(/Company ID is required/); + }); + + it('should handle API error responses', async () => { + mockHttpClient.get.mockRejectedValue(new Error('CT-e not found')); + + await expect(resource.retrieve(testCompanyId, validAccessKey)).rejects.toThrow('CT-e not found'); + }); + }); + + // ========================================================================== + // downloadXml() tests + // ========================================================================== + + describe('downloadXml', () => { + const mockXml = '...'; + + it('should download CT-e XML by access key', async () => { + const mockResponse: HttpResponse = { + data: mockXml, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.downloadXml(testCompanyId, validAccessKey); + + expect(result).toBe(mockXml); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/xml` + ); + }); + + it('should throw ValidationError for invalid access key', async () => { + await expect(resource.downloadXml(testCompanyId, 'invalid')).rejects.toThrow(ValidationError); + await expect(resource.downloadXml(testCompanyId, 'invalid')).rejects.toThrow(/Invalid access key/); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.downloadXml('', validAccessKey)).rejects.toThrow(ValidationError); + await expect(resource.downloadXml('', validAccessKey)).rejects.toThrow(/Company ID is required/); + }); + + it('should handle API error responses', async () => { + mockHttpClient.get.mockRejectedValue(new Error('XML not available')); + + await expect(resource.downloadXml(testCompanyId, validAccessKey)).rejects.toThrow('XML not available'); + }); + }); + + // ========================================================================== + // getEvent() tests + // ========================================================================== + + describe('getEvent', () => { + const mockEventMetadata: TransportationInvoiceMetadata = { + accessKey: validAccessKey, + type: 'Event', + nameSender: 'Test Sender Company', + federalTaxNumberSender: '12345678000190', + status: 'Authorized', + }; + + it('should retrieve CT-e event metadata', async () => { + const mockResponse: HttpResponse = { + data: mockEventMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getEvent(testCompanyId, validAccessKey, testEventKey); + + expect(result).toEqual(mockEventMetadata); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}` + ); + }); + + it('should handle event key with whitespace', async () => { + const mockResponse: HttpResponse = { + data: mockEventMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getEvent(testCompanyId, validAccessKey, ` ${testEventKey} `); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}` + ); + }); + + it('should throw ValidationError for empty event key', async () => { + await expect(resource.getEvent(testCompanyId, validAccessKey, '')).rejects.toThrow(ValidationError); + await expect(resource.getEvent(testCompanyId, validAccessKey, '')).rejects.toThrow(/Event key is required/); + }); + + it('should throw ValidationError for whitespace-only event key', async () => { + await expect(resource.getEvent(testCompanyId, validAccessKey, ' ')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for invalid access key', async () => { + await expect(resource.getEvent(testCompanyId, 'invalid', testEventKey)).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.getEvent('', validAccessKey, testEventKey)).rejects.toThrow(ValidationError); + }); + + it('should handle API error responses', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Event not found')); + + await expect(resource.getEvent(testCompanyId, validAccessKey, testEventKey)).rejects.toThrow('Event not found'); + }); + }); + + // ========================================================================== + // downloadEventXml() tests + // ========================================================================== + + describe('downloadEventXml', () => { + const mockEventXml = '...'; + + it('should download CT-e event XML', async () => { + const mockResponse: HttpResponse = { + data: mockEventXml, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.downloadEventXml(testCompanyId, validAccessKey, testEventKey); + + expect(result).toBe(mockEventXml); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}/xml` + ); + }); + + it('should throw ValidationError for empty event key', async () => { + await expect(resource.downloadEventXml(testCompanyId, validAccessKey, '')).rejects.toThrow(ValidationError); + await expect(resource.downloadEventXml(testCompanyId, validAccessKey, '')).rejects.toThrow(/Event key is required/); + }); + + it('should throw ValidationError for invalid access key', async () => { + await expect(resource.downloadEventXml(testCompanyId, 'short', testEventKey)).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty company ID', async () => { + await expect(resource.downloadEventXml('', validAccessKey, testEventKey)).rejects.toThrow(ValidationError); + }); + + it('should handle API error responses', async () => { + mockHttpClient.get.mockRejectedValue(new Error('Event XML not available')); + + await expect(resource.downloadEventXml(testCompanyId, validAccessKey, testEventKey)).rejects.toThrow('Event XML not available'); + }); + }); + + // ========================================================================== + // Access Key Validation (comprehensive tests) + // ========================================================================== + + describe('Access Key Validation', () => { + it('should accept valid 44-digit access key', async () => { + const mockResponse: HttpResponse = { + data: { accessKey: validAccessKey } as TransportationInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + // Should not throw + await expect(resource.retrieve(testCompanyId, validAccessKey)).resolves.toBeDefined(); + }); + + it('should reject access key with 43 digits', async () => { + const shortKey = '3524011234567800019057001000000123123456789'; // 43 digits + await expect(resource.retrieve(testCompanyId, shortKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should reject access key with 45 digits', async () => { + const longKey = '352401123456780001905700100000012312345678901'; // 45 digits + await expect(resource.retrieve(testCompanyId, longKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should reject access key with hyphen separators', async () => { + const hyphenatedKey = '3524-0112-3456-7800-0190-5700-1000-0001-2312-3456-7890'; + await expect(resource.retrieve(testCompanyId, hyphenatedKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should reject access key with spaces', async () => { + const spacedKey = '35240112345678000190 57001000000123123456789'; + await expect(resource.retrieve(testCompanyId, spacedKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should reject access key with special characters', async () => { + const specialKey = '3524011234567800019057001000000123123456789!'; + await expect(resource.retrieve(testCompanyId, specialKey)).rejects.toThrow(/Invalid access key/); + }); + + it('should include the invalid access key in error message', async () => { + const invalidKey = 'short-key'; + await expect(resource.retrieve(testCompanyId, invalidKey)).rejects.toThrow(`Invalid access key: "${invalidKey}"`); + }); + }); +});