From 8d4fcfd11306ee63aaf60bd1963f825068f8b31c Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Mon, 2 Feb 2026 22:13:33 -0300 Subject: [PATCH 1/3] feat: add 'openspec/' to .gitignore for improved dependency management --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5ea40c2..2b11495 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ node_modules/ bower_components/ jspm_packages/ +openspec/ # ---------------------------------------------------------------------------- # Build Outputs From 00aaffc139c1fbcf8bbbd3075e2783bd5ecb180d Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Mon, 2 Feb 2026 22:53:54 -0300 Subject: [PATCH 2/3] feat: add support for CT-e API with new configuration options and resource - Introduced `cteApiKey` in NfeConfig and RequiredNfeConfig interfaces for CT-e API key management. - Added TransportationInvoicesResource for handling CT-e operations, including enabling/disabling automatic searches, retrieving settings, and managing events. - Implemented comprehensive unit tests for TransportationInvoicesResource covering all functionalities and edge cases. - Updated NfeClient to support multi-API key configuration, including fallback mechanisms for CT-e API key. - Regenerated OpenAPI types and updated generated files with new timestamps. --- README.md | 72 +++ docs/API.md | 142 +++++ examples/setup.js | 5 + examples/transportation-invoices.js | 289 +++++++++++ src/core/client.ts | 94 ++++ src/core/resources/index.ts | 1 + src/core/resources/transportation-invoices.ts | 397 ++++++++++++++ src/core/types.ts | 47 ++ src/generated/calculo-impostos-v1.ts | 2 +- src/generated/consulta-cte-v2.ts | 2 +- src/generated/consulta-nfe-distribuicao-v1.ts | 2 +- src/generated/index.ts | 2 +- src/generated/nf-consumidor-v2.ts | 2 +- src/generated/nf-produto-v2.ts | 2 +- src/generated/nf-servico-v1.ts | 2 +- src/generated/nfeio.ts | 2 +- src/index.ts | 30 +- tests/unit/client-multikey.test.ts | 165 ++++++ .../resources/transportation-invoices.test.ts | 487 ++++++++++++++++++ 19 files changed, 1736 insertions(+), 9 deletions(-) create mode 100644 examples/transportation-invoices.js create mode 100644 src/core/resources/transportation-invoices.ts create mode 100644 tests/unit/resources/transportation-invoices.test.ts diff --git a/README.md b/README.md index 13922b6..b31fc55 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,72 @@ 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. +#### 🚚 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 `cteApiKey`, 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 + +--- + ### Opções de Configuração ```typescript @@ -373,6 +439,10 @@ const nfe = new NfeClient({ // Se não fornecida, usa apiKey como fallback addressApiKey: 'sua-chave-address-api', + // Opcional: Chave API específica para consulta de CT-e + // Se não fornecida, usa apiKey como fallback + cteApiKey: 'sua-chave-cte-api', + // Opcional: Ambiente (padrão: 'production') environment: 'production', // ou 'sandbox' @@ -400,11 +470,13 @@ O SDK suporta as seguintes variáveis de ambiente: |----------|-----------| | `NFE_API_KEY` | Chave API principal (fallback para `apiKey`) | | `NFE_ADDRESS_API_KEY` | Chave API para endereços (fallback para `addressApiKey`) | +| `NFE_CTE_API_KEY` | Chave API para CT-e (fallback para `cteApiKey`) | ```bash # Configurar via ambiente export NFE_API_KEY="sua-chave-api" export NFE_ADDRESS_API_KEY="sua-chave-address" +export NFE_CTE_API_KEY="sua-chave-cte" # Usar SDK sem passar chaves no código const nfe = new NfeClient({}); diff --git a/docs/API.md b/docs/API.md index 4adbbc3..bd28e32 100644 --- a/docs/API.md +++ b/docs/API.md @@ -15,6 +15,7 @@ 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) - [Types](#types) - [Error Handling](#error-handling) - [Advanced Usage](#advanced-usage) @@ -1504,6 +1505,145 @@ 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 `cteApiKey`, 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); +``` + +--- + ## Types ### Core Types @@ -1511,6 +1651,8 @@ const events = await nfe.webhooks.getAvailableEvents(); ```typescript interface NfeConfig { apiKey?: string; + addressApiKey?: string; // Specific API key for address lookups + cteApiKey?: string; // Specific API key for CT-e (transportation invoices) environment?: 'production' | 'development'; baseUrl?: string; timeout?: number; diff --git a/examples/setup.js b/examples/setup.js index 2a7a4d7..c6ace65 100644 --- a/examples/setup.js +++ b/examples/setup.js @@ -112,6 +112,11 @@ ${companyId ? `NFE_COMPANY_ID=${companyId}` : '# NFE_COMPANY_ID=seu-company-id-a # Timeout em ms (opcional) # NFE_TIMEOUT=30000 + +# Chaves de API específicas (opcional) +# Se não definidas, usa NFE_API_KEY como fallback +# NFE_ADDRESS_API_KEY=sua-chave-address-aqui +# NFE_CTE_API_KEY=sua-chave-cte-aqui `; try { diff --git a/examples/transportation-invoices.js b/examples/transportation-invoices.js new file mode 100644 index 0000000..3364596 --- /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_CTE_API_KEY - Specific CT-e API key (recommended) + * - NFE_API_KEY - Main API key (will be used as fallback) + * + * Or configure in code: + * const nfe = new NfeClient({ + * cteApiKey: 'your-cte-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. cteApiKey (config) +// 2. apiKey (config) +// 3. NFE_CTE_API_KEY (env) +// 4. NFE_API_KEY (env) +const nfe = new NfeClient({ + // cteApiKey: process.env.NFE_CTE_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_CTE_API_KEY or NFE_API_KEY'); + } + if (error.name === 'ValidationError') { + console.error(' Check your input parameters'); + } + process.exit(1); + } +} + +main(); diff --git a/src/core/client.ts b/src/core/client.ts index 34f7b01..937ec1b 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -27,9 +27,17 @@ import { NaturalPeopleResource, WebhooksResource, AddressesResource, + TransportationInvoicesResource, ADDRESS_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'; + // ============================================================================ // Main NFE.io Client // ============================================================================ @@ -113,6 +121,9 @@ 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 Normalized client configuration */ private readonly config: RequiredNfeConfig; @@ -123,6 +134,7 @@ export class NfeClient { private _naturalPeople: NaturalPeopleResource | undefined; private _webhooks: WebhooksResource | undefined; private _addresses: AddressesResource | undefined; + private _transportationInvoices: TransportationInvoicesResource | undefined; /** * Service Invoices API resource @@ -291,6 +303,45 @@ 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 `cteApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link TransportationInvoicesResource} + * @throws {ConfigurationError} If no API key is configured (cteApiKey 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; + } + /** * Create a new NFE.io API client * @@ -420,6 +471,42 @@ export class NfeClient { ); } + /** + * Resolve the CT-e API key using fallback chain + * Order: cteApiKey → apiKey → NFE_CTE_API_KEY → NFE_API_KEY + */ + private resolveCteApiKey(): string | undefined { + return ( + this.config.cteApiKey || + this.config.apiKey || + this.getEnvironmentVariable('NFE_CTE_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.resolveCteApiKey(); + if (!apiKey) { + throw new ConfigurationError( + 'API key required for Transportation Invoices (CT-e). Set "cteApiKey" or "apiKey" in config, or NFE_CTE_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; + } + // -------------------------------------------------------------------------- // Configuration Management // -------------------------------------------------------------------------- @@ -428,6 +515,7 @@ export class NfeClient { // API keys are now optional - validated lazily when resources are accessed const apiKey = config.apiKey?.trim() || undefined; const addressApiKey = config.addressApiKey?.trim() || undefined; + const cteApiKey = config.cteApiKey?.trim() || undefined; // Normalize environment const environment = config.environment || 'production'; @@ -447,6 +535,7 @@ export class NfeClient { const normalizedConfig: RequiredNfeConfig = { apiKey, addressApiKey, + cteApiKey, environment, baseUrl: config.baseUrl || this.getDefaultBaseUrl(), timeout: config.timeout || 30000, @@ -546,6 +635,9 @@ export class NfeClient { if (normalizedConfig.addressApiKey === undefined && this.config.addressApiKey !== undefined && newConfig.addressApiKey === undefined) { normalizedConfig.addressApiKey = this.config.addressApiKey; } + if (normalizedConfig.cteApiKey === undefined && this.config.cteApiKey !== undefined && newConfig.cteApiKey === undefined) { + normalizedConfig.cteApiKey = this.config.cteApiKey; + } // Update internal config Object.assign(this.config, normalizedConfig); @@ -553,12 +645,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/resources/index.ts b/src/core/resources/index.ts index 4317456..6f89faa 100644 --- a/src/core/resources/index.ts +++ b/src/core/resources/index.ts @@ -11,3 +11,4 @@ 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'; diff --git a/src/core/resources/transportation-invoices.ts b/src/core/resources/transportation-invoices.ts new file mode 100644 index 0000000..2276162 --- /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 `cteApiKey` 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..cb6fb0e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -19,6 +19,8 @@ export interface NfeConfig { apiKey?: string; /** NFE.io API Key specifically for Address API (optional, falls back to apiKey) */ addressApiKey?: string; + /** NFE.io API Key specifically for CT-e API (optional, falls back to apiKey) */ + cteApiKey?: string; /** Environment to use (both use same endpoint, differentiated by API key) */ environment?: 'production' | 'development'; /** Custom base URL (overrides environment) */ @@ -298,6 +300,8 @@ export interface RequiredNfeConfig { apiKey: string | undefined; /** Address API key (may be undefined, will fallback to apiKey) */ addressApiKey: string | undefined; + /** CT-e API key (may be undefined, will fallback to apiKey) */ + cteApiKey: string | undefined; /** Environment */ environment: 'production' | 'development'; /** Base URL for main API */ @@ -394,3 +398,46 @@ 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']; diff --git a/src/generated/calculo-impostos-v1.ts b/src/generated/calculo-impostos-v1.ts index a6f46a5..84f84c2 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-03T01:51:28.320Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-cte-v2.ts b/src/generated/consulta-cte-v2.ts index 301c89d..4ece182 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-03T01:51:28.344Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-nfe-distribuicao-v1.ts b/src/generated/consulta-nfe-distribuicao-v1.ts index 31e8515..92731a1 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-03T01:51:28.373Z * Generator: openapi-typescript */ diff --git a/src/generated/index.ts b/src/generated/index.ts index efdb072..dfd4f82 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-03T01:51:28.621Z */ // ============================================================================ diff --git a/src/generated/nf-consumidor-v2.ts b/src/generated/nf-consumidor-v2.ts index 0717bb9..3ea559c 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-03T01:51:28.469Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-produto-v2.ts b/src/generated/nf-produto-v2.ts index c59764e..8648a84 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-03T01:51:28.551Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-servico-v1.ts b/src/generated/nf-servico-v1.ts index 1351271..7b22f23 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-03T01:51:28.610Z * Generator: openapi-typescript */ diff --git a/src/generated/nfeio.ts b/src/generated/nfeio.ts index aeefafa..c29da9c 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-03T01:51:28.620Z * Generator: openapi-typescript */ diff --git a/src/index.ts b/src/index.ts index 31ba08f..f3daaf2 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 } from './core/client.js'; /** * TypeScript type definitions for NFE.io API entities and configurations @@ -81,6 +81,13 @@ export type { AddressLookupResponse, AddressSearchOptions, + // CT-e (Transportation Invoice) types + TransportationInvoiceInboundSettings, + TransportationInvoiceMetadata, + EnableTransportationInvoiceOptions, + TransportationInvoiceEntityStatus, + TransportationInvoiceMetadataType, + // Common types EntityType, TaxRegime, @@ -175,6 +182,27 @@ 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'; + // ============================================================================ // Default Export (maintains v2 compatibility) // ============================================================================ diff --git a/tests/unit/client-multikey.test.ts b/tests/unit/client-multikey.test.ts index 91fe276..828dd5a 100644 --- a/tests/unit/client-multikey.test.ts +++ b/tests/unit/client-multikey.test.ts @@ -14,6 +14,7 @@ describe('NfeClient Multi-API Key Support', () => { // 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_CTE_API_KEY; }); afterEach(() => { @@ -235,5 +236,169 @@ describe('NfeClient Multi-API Key Support', () => { /addressApiKey|apiKey/ ); }); + + it('should have descriptive error for missing CTE API key', () => { + const client = new NfeClient({}); + + expect(() => client.transportationInvoices).toThrow( + /cteApiKey|apiKey/ + ); + }); + }); + + describe('API key fallback chain for CTE API', () => { + beforeEach(() => { + // Clear CTE-specific env vars + delete process.env.NFE_CTE_API_KEY; + }); + + it('should use cteApiKey from config', () => { + const client = new NfeClient({ cteApiKey: 'cte-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 cteApiKey not specified + expect(() => client.transportationInvoices).not.toThrow(); + }); + + it('should fall back to NFE_CTE_API_KEY environment variable', () => { + process.env.NFE_CTE_API_KEY = 'env-cte-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 cteApiKey over apiKey', () => { + const client = new NfeClient({ + apiKey: 'main-key', + cteApiKey: 'cte-key', + }); + + expect(() => client.transportationInvoices).not.toThrow(); + const config = client.getConfig(); + expect(config.cteApiKey).toBe('cte-key'); + }); + + it('should prefer config keys over environment variables', () => { + process.env.NFE_CTE_API_KEY = 'env-cte-key'; + process.env.NFE_API_KEY = 'env-main-key'; + + const client = new NfeClient({ cteApiKey: 'config-cte-key' }); + + expect(() => client.transportationInvoices).not.toThrow(); + const config = client.getConfig(); + expect(config.cteApiKey).toBe('config-cte-key'); + }); + + it('should throw ConfigurationError when accessing transportationInvoices without any apiKey', () => { + const client = new NfeClient({}); + + expect(() => client.transportationInvoices).toThrow(ConfigurationError); + expect(() => client.transportationInvoices).toThrow(/cteApiKey|apiKey/); + }); + }); + + describe('isolated CTE resource usage', () => { + beforeEach(() => { + delete process.env.NFE_CTE_API_KEY; + }); + + it('should allow using only transportationInvoices with cteApiKey (no apiKey)', () => { + const client = new NfeClient({ cteApiKey: 'cte-only-key' }); + + // Transportation invoices should work + expect(() => client.transportationInvoices).not.toThrow(); + + // Other resources should throw + expect(() => client.serviceInvoices).toThrow(ConfigurationError); + expect(() => client.companies).toThrow(ConfigurationError); + }); + + it('should allow using only main resources with apiKey (no cteApiKey)', () => { + const client = new NfeClient({ apiKey: 'main-only-key' }); + + // Main resources should work + expect(() => client.serviceInvoices).not.toThrow(); + expect(() => client.companies).not.toThrow(); + + // Transportation invoices should also work (falls back to apiKey) + expect(() => client.transportationInvoices).not.toThrow(); + }); + + it('should support separate API keys for CTE and main resources', () => { + const client = new NfeClient({ + apiKey: 'main-api-key', + cteApiKey: 'separate-cte-key', + }); + + // All resources should work + expect(() => client.serviceInvoices).not.toThrow(); + expect(() => client.companies).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.cteApiKey).toBe('separate-cte-key'); + }); + + it('should support all three separate API keys', () => { + const client = new NfeClient({ + apiKey: 'main-api-key', + addressApiKey: 'separate-address-key', + cteApiKey: 'separate-cte-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 all keys + const config = client.getConfig(); + expect(config.apiKey).toBe('main-api-key'); + expect(config.addressApiKey).toBe('separate-address-key'); + expect(config.cteApiKey).toBe('separate-cte-key'); + }); + }); + + describe('CTE resource caching', () => { + beforeEach(() => { + delete process.env.NFE_CTE_API_KEY; + }); + + it('should cache transportationInvoices resource', () => { + const client = new NfeClient({ cteApiKey: 'test-key' }); + + const transportationInvoices1 = client.transportationInvoices; + const transportationInvoices2 = client.transportationInvoices; + + expect(transportationInvoices1).toBe(transportationInvoices2); + }); + + it('should clear transportationInvoices cache on updateConfig', () => { + const client = new NfeClient({ cteApiKey: 'initial-key' }); + + const transportationInvoices1 = client.transportationInvoices; + + client.updateConfig({ cteApiKey: 'new-key' }); + + const transportationInvoices2 = client.transportationInvoices; + + // Resource should be a new instance + expect(transportationInvoices1).not.toBe(transportationInvoices2); + }); }); }); 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}"`); + }); + }); +}); From 6d9ba3ba1552350d54da00a645fb628315b7560a Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Fri, 13 Feb 2026 21:38:12 -0300 Subject: [PATCH 3/3] refactor(config): unifica addressApiKey e cteApiKey em dataApiKey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolida as chaves de API separadas para serviços de consulta (addressApiKey e cteApiKey) em uma única propriedade dataApiKey, simplificando a configuração do SDK e refletindo a arquitetura real da API NFE.io (duas categorias: fiscal e consulta). Motivação: - A API NFE.io utiliza apenas duas categorias de chave: uma para operações fiscais (NFS-e, Companies, etc.) e outra para todos os serviços de consulta (Endereços, CT-e, CNPJ, CPF) - Ter chaves separadas (addressApiKey, cteApiKey) adicionava complexidade desnecessária sem benefício real - A unificação prepara o SDK para futuros serviços de consulta que usarão a mesma chave Alterações em tipos (src/core/types.ts): - NfeConfig: remove addressApiKey e cteApiKey, adiciona dataApiKey - RequiredNfeConfig: idem, mantendo tipagem string | undefined Alterações no client (src/core/client.ts): - Remove resolveAddressApiKey() e resolveCteApiKey() - Adiciona resolveDataApiKey() com cadeia de fallback: config.dataApiKey → config.apiKey → NFE_DATA_API_KEY → NFE_API_KEY - getAddressHttpClient() e getCteHttpClient() usam resolveDataApiKey() - validateAndNormalizeConfig() normaliza dataApiKey ao invés de duas - updateConfig() preserva cache de dataApiKey ao invés de duas - Mensagens de erro atualizadas para referenciar dataApiKey - JSDoc atualizado nos getters addresses e transportationInvoices Alterações em resources: - addresses.ts: JSDoc atualizado para referenciar dataApiKey - transportation-invoices.ts: JSDoc atualizado para referenciar dataApiKey Alterações em testes: - tests/unit/client-multikey.test.ts: reescrito (38 testes) - Testa cadeia de fallback para Addresses e CT-e via dataApiKey - Testa que ambos services resolvem a mesma chave - Testa uso isolado de dataApiKey sem apiKey - Testa que NFE_ADDRESS_API_KEY e NFE_CTE_API_KEY não são mais reconhecidas (breaking change documentada) - tests/integration/addresses.integration.test.ts: atualizado Alterações em documentação: - README.md: config examples, tabela env vars (2 vars ao invés de 3), notas sobre Address API e CT-e - docs/API.md: NfeConfig type, nota do recurso CT-e Alterações em exemplos: - examples/setup.js: template .env.test usa NFE_DATA_API_KEY - examples/address-lookup.js: config e env vars atualizados - examples/transportation-invoices.js: config e env vars atualizados Arquivos gerados (src/generated/): - Timestamps atualizados pela regeneração do openapi-typescript Validação: - TypeScript: zero erros (npx tsc --noEmit) - Testes: 381 passed, 47 skipped (integration) - Build: ESM + CJS + DTS gerados com sucesso BREAKING CHANGE: addressApiKey e cteApiKey foram removidos de NfeConfig. Use dataApiKey para configurar a chave de serviços de consulta. As variáveis de ambiente NFE_ADDRESS_API_KEY e NFE_CTE_API_KEY foram substituídas por NFE_DATA_API_KEY. --- README.md | 20 +- docs/API.md | 5 +- examples/address-lookup.js | 28 +- examples/setup.js | 7 +- examples/transportation-invoices.js | 12 +- src/core/client.ts | 58 ++-- src/core/resources/addresses.ts | 2 +- src/core/resources/transportation-invoices.ts | 2 +- src/core/types.ts | 14 +- src/generated/calculo-impostos-v1.ts | 2 +- src/generated/consulta-cte-v2.ts | 2 +- src/generated/consulta-nfe-distribuicao-v1.ts | 2 +- src/generated/index.ts | 2 +- src/generated/nf-consumidor-v2.ts | 2 +- src/generated/nf-produto-v2.ts | 2 +- src/generated/nf-servico-v1.ts | 2 +- src/generated/nfeio.ts | 2 +- .../integration/addresses.integration.test.ts | 16 +- tests/unit/client-multikey.test.ts | 306 ++++++++---------- 19 files changed, 212 insertions(+), 274 deletions(-) diff --git a/README.md b/README.md index b31fc55..13c02fa 100644 --- a/README.md +++ b/README.md @@ -360,7 +360,7 @@ 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`) @@ -420,7 +420,7 @@ const eventoXml = await nfe.transportationInvoices.downloadEventXml( ); ``` -> **Nota:** A API de CT-e usa um host separado (`api.nfse.io`). Você pode configurar uma chave API específica com `cteApiKey`, ou o SDK usará `apiKey` como fallback. +> **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 @@ -432,16 +432,12 @@ const eventoXml = await nfe.transportationInvoices.downloadEventXml( ```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', - - // Opcional: Chave API específica para consulta de CT-e - // Se não fornecida, usa apiKey como fallback - cteApiKey: 'sua-chave-cte-api', + dataApiKey: 'sua-chave-data-api', // Opcional: Ambiente (padrão: 'production') environment: 'production', // ou 'sandbox' @@ -469,14 +465,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_CTE_API_KEY` | Chave API para CT-e (fallback para `cteApiKey`) | +| `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_CTE_API_KEY="sua-chave-cte" +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 bd28e32..375ae72 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1513,7 +1513,7 @@ const events = await nfe.webhooks.getAvailableEvents(); 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 `cteApiKey`, or the SDK will use `apiKey` as fallback. +> **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 @@ -1651,8 +1651,7 @@ fs.writeFileSync('cte-event.xml', eventXml); ```typescript interface NfeConfig { apiKey?: string; - addressApiKey?: string; // Specific API key for address lookups - cteApiKey?: string; // Specific API key for CT-e (transportation invoices) + dataApiKey?: string; // API key for data/query services (Addresses, CT-e, CNPJ, CPF) environment?: 'production' | 'development'; baseUrl?: string; timeout?: number; 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/setup.js b/examples/setup.js index c6ace65..a0f5707 100644 --- a/examples/setup.js +++ b/examples/setup.js @@ -113,10 +113,9 @@ ${companyId ? `NFE_COMPANY_ID=${companyId}` : '# NFE_COMPANY_ID=seu-company-id-a # Timeout em ms (opcional) # NFE_TIMEOUT=30000 -# Chaves de API específicas (opcional) -# Se não definidas, usa NFE_API_KEY como fallback -# NFE_ADDRESS_API_KEY=sua-chave-address-aqui -# NFE_CTE_API_KEY=sua-chave-cte-aqui +# 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/transportation-invoices.js b/examples/transportation-invoices.js index 3364596..232f245 100644 --- a/examples/transportation-invoices.js +++ b/examples/transportation-invoices.js @@ -11,12 +11,12 @@ * * Configuration: * Set one of the following environment variables: - * - NFE_CTE_API_KEY - Specific CT-e API key (recommended) + * - 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({ - * cteApiKey: 'your-cte-api-key', // Or use apiKey if you have unified access + * dataApiKey: 'your-data-api-key', // Or use apiKey if you have unified access * }); * * Usage: @@ -34,12 +34,12 @@ import { NfeClient } from 'nfe-io'; // ============================================================================ // Create client - API key fallback chain: -// 1. cteApiKey (config) +// 1. dataApiKey (config) // 2. apiKey (config) -// 3. NFE_CTE_API_KEY (env) +// 3. NFE_DATA_API_KEY (env) // 4. NFE_API_KEY (env) const nfe = new NfeClient({ - // cteApiKey: process.env.NFE_CTE_API_KEY, // Uncomment for explicit configuration + // dataApiKey: process.env.NFE_DATA_API_KEY, // Uncomment for explicit configuration }); // ============================================================================ @@ -277,7 +277,7 @@ async function main() { } catch (error) { console.error('\n❌ Error:', error.message); if (error.name === 'ConfigurationError') { - console.error(' Make sure you have set NFE_CTE_API_KEY or NFE_API_KEY'); + 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'); diff --git a/src/core/client.ts b/src/core/client.ts index 937ec1b..ea22610 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -285,10 +285,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 @@ -318,10 +318,10 @@ export class NfeClient { * - Webhook must be configured to receive CT-e notifications * * **Note:** This resource uses a different API host (api.nfse.io). - * Configure `cteApiKey` for a separate key, or it will fallback to `apiKey`. + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. * * @see {@link TransportationInvoicesResource} - * @throws {ConfigurationError} If no API key is configured (cteApiKey or apiKey) + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) * * @example * ```typescript @@ -379,11 +379,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'); * ``` @@ -431,10 +431,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( @@ -459,27 +459,14 @@ 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_API_KEY') - ); - } - - /** - * Resolve the CT-e API key using fallback chain - * Order: cteApiKey → apiKey → NFE_CTE_API_KEY → NFE_API_KEY - */ - private resolveCteApiKey(): string | undefined { - return ( - this.config.cteApiKey || - this.config.apiKey || - this.getEnvironmentVariable('NFE_CTE_API_KEY') || + this.getEnvironmentVariable('NFE_DATA_API_KEY') || this.getEnvironmentVariable('NFE_API_KEY') ); } @@ -490,10 +477,10 @@ export class NfeClient { */ private getCteHttpClient(): HttpClient { if (!this._cteHttp) { - const apiKey = this.resolveCteApiKey(); + const apiKey = this.resolveDataApiKey(); if (!apiKey) { throw new ConfigurationError( - 'API key required for Transportation Invoices (CT-e). Set "cteApiKey" or "apiKey" in config, or NFE_CTE_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( @@ -514,8 +501,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 cteApiKey = config.cteApiKey?.trim() || undefined; + const dataApiKey = config.dataApiKey?.trim() || undefined; // Normalize environment const environment = config.environment || 'production'; @@ -534,8 +520,7 @@ export class NfeClient { const normalizedConfig: RequiredNfeConfig = { apiKey, - addressApiKey, - cteApiKey, + dataApiKey, environment, baseUrl: config.baseUrl || this.getDefaultBaseUrl(), timeout: config.timeout || 30000, @@ -632,11 +617,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.cteApiKey === undefined && this.config.cteApiKey !== undefined && newConfig.cteApiKey === undefined) { - normalizedConfig.cteApiKey = this.config.cteApiKey; + if (normalizedConfig.dataApiKey === undefined && this.config.dataApiKey !== undefined && newConfig.dataApiKey === undefined) { + normalizedConfig.dataApiKey = this.config.dataApiKey; } // Update internal config 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/transportation-invoices.ts b/src/core/resources/transportation-invoices.ts index 2276162..1093696 100644 --- a/src/core/resources/transportation-invoices.ts +++ b/src/core/resources/transportation-invoices.ts @@ -73,7 +73,7 @@ function validateCompanyId(companyId: string): void { * - 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 `cteApiKey` in the client configuration. + * 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 diff --git a/src/core/types.ts b/src/core/types.ts index cb6fb0e..bf3f18b 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -17,10 +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 specifically for CT-e API (optional, falls back to apiKey) */ - cteApiKey?: 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) */ @@ -296,12 +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; - /** CT-e API key (may be undefined, will fallback to apiKey) */ - cteApiKey: 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 */ diff --git a/src/generated/calculo-impostos-v1.ts b/src/generated/calculo-impostos-v1.ts index 84f84c2..26a4735 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-02-03T01:51:28.320Z + * Last generated: 2026-02-14T00:32:45.806Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-cte-v2.ts b/src/generated/consulta-cte-v2.ts index 4ece182..bbf5638 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-02-03T01:51:28.344Z + * Last generated: 2026-02-14T00:32:45.826Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-nfe-distribuicao-v1.ts b/src/generated/consulta-nfe-distribuicao-v1.ts index 92731a1..9afc65b 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-02-03T01:51:28.373Z + * Last generated: 2026-02-14T00:32:45.857Z * Generator: openapi-typescript */ diff --git a/src/generated/index.ts b/src/generated/index.ts index dfd4f82..393bb48 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-02-03T01:51:28.621Z + * Last updated: 2026-02-14T00:32:46.103Z */ // ============================================================================ diff --git a/src/generated/nf-consumidor-v2.ts b/src/generated/nf-consumidor-v2.ts index 3ea559c..dc2bb30 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-02-03T01:51:28.469Z + * Last generated: 2026-02-14T00:32:45.948Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-produto-v2.ts b/src/generated/nf-produto-v2.ts index 8648a84..e32e771 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-02-03T01:51:28.551Z + * Last generated: 2026-02-14T00:32:46.031Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-servico-v1.ts b/src/generated/nf-servico-v1.ts index 7b22f23..99a2faf 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-02-03T01:51:28.610Z + * Last generated: 2026-02-14T00:32:46.092Z * Generator: openapi-typescript */ diff --git a/src/generated/nfeio.ts b/src/generated/nfeio.ts index c29da9c..e42a71b 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-02-03T01:51:28.620Z + * Last generated: 2026-02-14T00:32:46.102Z * Generator: openapi-typescript */ 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 828dd5a..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,8 +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_CTE_API_KEY; + delete process.env.NFE_DATA_API_KEY; }); afterEach(() => { @@ -59,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', () => { @@ -95,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(); }); @@ -105,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(); @@ -123,137 +126,32 @@ 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('isolated resource usage', () => { - it('should allow using only addresses with addressApiKey (no apiKey)', () => { - const client = new NfeClient({ addressApiKey: 'address-only-key' }); - - // Addresses should work - expect(() => client.addresses).not.toThrow(); - - // Other resources should throw - expect(() => client.serviceInvoices).toThrow(ConfigurationError); - expect(() => client.companies).toThrow(ConfigurationError); - }); - - it('should allow using only main resources with apiKey (no addressApiKey)', () => { - 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) - expect(() => client.addresses).not.toThrow(); - }); - - it('should support separate API keys for different resources', () => { - const client = new NfeClient({ - apiKey: 'main-api-key', - addressApiKey: 'separate-address-key', - }); - - // All resources should work - expect(() => client.serviceInvoices).not.toThrow(); - expect(() => client.companies).not.toThrow(); - expect(() => client.addresses).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'); - }); - }); - - describe('resource caching', () => { - it('should cache resource instances', () => { - const client = new NfeClient({ apiKey: 'test-key' }); - - const serviceInvoices1 = client.serviceInvoices; - const serviceInvoices2 = client.serviceInvoices; - - expect(serviceInvoices1).toBe(serviceInvoices2); - }); - - it('should cache addresses resource', () => { - const client = new NfeClient({ addressApiKey: 'test-key' }); - - const addresses1 = client.addresses; - const addresses2 = client.addresses; - - expect(addresses1).toBe(addresses2); - }); - - it('should clear cache on updateConfig', () => { - const client = new NfeClient({ apiKey: 'initial-key' }); - - const serviceInvoices1 = client.serviceInvoices; - - client.updateConfig({ apiKey: 'new-key' }); - - const serviceInvoices2 = client.serviceInvoices; - - // Resource should be a new instance - expect(serviceInvoices1).not.toBe(serviceInvoices2); - }); - }); - - describe('error messages', () => { - it('should have descriptive error for missing main API key', () => { - const client = new NfeClient({}); - - expect(() => client.serviceInvoices).toThrow( - 'API key required for this resource. Set "apiKey" in config or NFE_API_KEY environment variable.' - ); - }); - - it('should have descriptive error for missing address API key', () => { - const client = new NfeClient({}); - - expect(() => client.addresses).toThrow( - /addressApiKey|apiKey/ - ); - }); - - it('should have descriptive error for missing CTE API key', () => { - const client = new NfeClient({}); - - expect(() => client.transportationInvoices).toThrow( - /cteApiKey|apiKey/ - ); - }); - }); - - describe('API key fallback chain for CTE API', () => { - beforeEach(() => { - // Clear CTE-specific env vars - delete process.env.NFE_CTE_API_KEY; - }); - - it('should use cteApiKey from config', () => { - const client = new NfeClient({ cteApiKey: 'cte-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(); }); @@ -261,12 +159,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 CTE when cteApiKey not specified + // Should use main apiKey for CTE when dataApiKey not specified expect(() => client.transportationInvoices).not.toThrow(); }); - it('should fall back to NFE_CTE_API_KEY environment variable', () => { - process.env.NFE_CTE_API_KEY = 'env-cte-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.transportationInvoices).not.toThrow(); @@ -279,108 +177,123 @@ describe('NfeClient Multi-API Key Support', () => { expect(() => client.transportationInvoices).not.toThrow(); }); - it('should prefer cteApiKey over apiKey', () => { + it('should prefer dataApiKey over apiKey', () => { const client = new NfeClient({ apiKey: 'main-key', - cteApiKey: 'cte-key', + dataApiKey: 'data-key', }); expect(() => client.transportationInvoices).not.toThrow(); const config = client.getConfig(); - expect(config.cteApiKey).toBe('cte-key'); + expect(config.dataApiKey).toBe('data-key'); }); it('should prefer config keys over environment variables', () => { - process.env.NFE_CTE_API_KEY = 'env-cte-key'; + process.env.NFE_DATA_API_KEY = 'env-data-key'; process.env.NFE_API_KEY = 'env-main-key'; - const client = new NfeClient({ cteApiKey: 'config-cte-key' }); + const client = new NfeClient({ dataApiKey: 'config-data-key' }); expect(() => client.transportationInvoices).not.toThrow(); const config = client.getConfig(); - expect(config.cteApiKey).toBe('config-cte-key'); + 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(/cteApiKey|apiKey/); + expect(() => client.transportationInvoices).toThrow(/dataApiKey|apiKey/); }); }); - describe('isolated CTE resource usage', () => { - beforeEach(() => { - delete process.env.NFE_CTE_API_KEY; + 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(); }); + }); - it('should allow using only transportationInvoices with cteApiKey (no apiKey)', () => { - const client = new NfeClient({ cteApiKey: 'cte-only-key' }); + describe('isolated resource usage', () => { + it('should allow using only data services with dataApiKey (no apiKey)', () => { + const client = new NfeClient({ dataApiKey: 'data-only-key' }); - // Transportation invoices 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 cteApiKey)', () => { + 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(); - // Transportation invoices 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 CTE and main resources', () => { + it('should support separate API keys for data and main resources', () => { const client = new NfeClient({ apiKey: 'main-api-key', - cteApiKey: 'separate-cte-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.cteApiKey).toBe('separate-cte-key'); + expect(config.dataApiKey).toBe('separate-data-key'); }); + }); - it('should support all three separate API keys', () => { - const client = new NfeClient({ - apiKey: 'main-api-key', - addressApiKey: 'separate-address-key', - cteApiKey: 'separate-cte-key', - }); + describe('resource caching', () => { + it('should cache resource instances', () => { + const client = new NfeClient({ apiKey: 'test-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(); + const serviceInvoices1 = client.serviceInvoices; + const serviceInvoices2 = client.serviceInvoices; - // Verify config has all keys - const config = client.getConfig(); - expect(config.apiKey).toBe('main-api-key'); - expect(config.addressApiKey).toBe('separate-address-key'); - expect(config.cteApiKey).toBe('separate-cte-key'); + expect(serviceInvoices1).toBe(serviceInvoices2); }); - }); - describe('CTE resource caching', () => { - beforeEach(() => { - delete process.env.NFE_CTE_API_KEY; + it('should cache addresses resource', () => { + const client = new NfeClient({ dataApiKey: 'test-key' }); + + const addresses1 = client.addresses; + const addresses2 = client.addresses; + + expect(addresses1).toBe(addresses2); }); it('should cache transportationInvoices resource', () => { - const client = new NfeClient({ cteApiKey: 'test-key' }); + const client = new NfeClient({ dataApiKey: 'test-key' }); const transportationInvoices1 = client.transportationInvoices; const transportationInvoices2 = client.transportationInvoices; @@ -388,12 +301,25 @@ describe('NfeClient Multi-API Key Support', () => { expect(transportationInvoices1).toBe(transportationInvoices2); }); - it('should clear transportationInvoices cache on updateConfig', () => { - const client = new NfeClient({ cteApiKey: 'initial-key' }); + it('should clear cache on updateConfig', () => { + const client = new NfeClient({ apiKey: 'initial-key' }); + + const serviceInvoices1 = client.serviceInvoices; + + client.updateConfig({ apiKey: 'new-key' }); + + const serviceInvoices2 = client.serviceInvoices; + + // 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({ cteApiKey: 'new-key' }); + client.updateConfig({ dataApiKey: 'new-key' }); const transportationInvoices2 = client.transportationInvoices; @@ -401,4 +327,46 @@ describe('NfeClient Multi-API Key Support', () => { expect(transportationInvoices1).not.toBe(transportationInvoices2); }); }); + + describe('error messages', () => { + it('should have descriptive error for missing main API key', () => { + const client = new NfeClient({}); + + expect(() => client.serviceInvoices).toThrow( + 'API key required for this resource. Set "apiKey" in config or NFE_API_KEY environment variable.' + ); + }); + + it('should have descriptive error for missing data API key (addresses)', () => { + const client = new NfeClient({}); + + expect(() => client.addresses).toThrow( + /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); + }); + }); });