diff --git a/README.md b/README.md index 13c02fa..e6aa58d 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,97 @@ const eventoXml = await nfe.transportationInvoices.downloadEventXml( - Empresa deve estar cadastrada com certificado digital A1 válido - Webhook deve estar configurado para receber notificações de CT-e +#### 📦 NF-e de Entrada - Distribuição (`nfe.inboundProductInvoices`) + +Consultar NF-e (Nota Fiscal Eletrônica de Produto) recebidas via Distribuição NF-e: + +```typescript +// Ativar busca automática de NF-e para uma empresa +const settings = await nfe.inboundProductInvoices.enableAutoFetch('empresa-id', { + environmentSEFAZ: 'Production', + webhookVersion: '2', +}); +console.log('Status:', settings.status); + +// Ativar a partir de um NSU específico +const settings = await nfe.inboundProductInvoices.enableAutoFetch('empresa-id', { + startFromNsu: '999999', + environmentSEFAZ: 'Production', +}); + +// Verificar configurações atuais +const config = await nfe.inboundProductInvoices.getSettings('empresa-id'); +console.log('Busca ativa:', config.status); + +// Desativar busca automática +await nfe.inboundProductInvoices.disableAutoFetch('empresa-id'); + +// Consultar NF-e por chave de acesso - formato webhook v2 (recomendado) +const nfe_doc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); +console.log('Emissor:', nfe_doc.issuer?.name); +console.log('Valor:', nfe_doc.totalInvoiceAmount); + +// Baixar XML da NF-e +const xml = await nfe.inboundProductInvoices.getXml( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('nfe.xml', xml); + +// Baixar PDF (DANFE) +const pdf = await nfe.inboundProductInvoices.getPdf( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); + +// Enviar manifestação (Ciência da Operação por padrão) +await nfe.inboundProductInvoices.manifest( + 'empresa-id', + '35240112345678000190550010000001231234567890' +); + +// Manifestar com evento específico +await nfe.inboundProductInvoices.manifest( + 'empresa-id', + '35240112345678000190550010000001231234567890', + 210220 // Confirmação da Operação +); + +// Consultar evento da NF-e +const evento = await nfe.inboundProductInvoices.getEventDetails( + 'empresa-id', + '35240112345678000190550010000001231234567890', + 'chave-evento' +); + +// Baixar XML do evento +const eventoXml = await nfe.inboundProductInvoices.getEventXml( + 'empresa-id', + '35240112345678000190550010000001231234567890', + 'chave-evento' +); + +// Reprocessar webhook +await nfe.inboundProductInvoices.reprocessWebhook('empresa-id', '35240...'); +``` + +> **Nota:** A API de NF-e Distribuição usa um host separado (`api.nfse.io`). Você pode configurar uma chave API específica com `dataApiKey`, ou o SDK usará `apiKey` como fallback. + +**Pré-requisitos:** +- Empresa deve estar cadastrada com certificado digital A1 válido +- Webhook deve estar configurado para receber notificações de NF-e + +**Tipos de Manifestação:** + +| Código | Evento | +|--------|--------| +| `210210` | Ciência da Operação (padrão) | +| `210220` | Confirmação da Operação | +| `210240` | Operação não Realizada | + --- ### Opções de Configuração diff --git a/docs/API.md b/docs/API.md index 375ae72..b53378a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,6 +16,7 @@ Complete API reference for the NFE.io Node.js SDK v3. - [Natural People](#natural-people) - [Webhooks](#webhooks) - [Transportation Invoices (CT-e)](#transportation-invoices-ct-e) + - [Inbound Product Invoices (NF-e Distribuição)](#inbound-product-invoices-nf-e-distribuição) - [Types](#types) - [Error Handling](#error-handling) - [Advanced Usage](#advanced-usage) @@ -1644,6 +1645,256 @@ fs.writeFileSync('cte-event.xml', eventXml); --- +### Inbound Product Invoices (NF-e Distribuição) + +**Resource:** `nfe.inboundProductInvoices` + +Query NF-e (Nota Fiscal Eletrônica de Produto) documents received via SEFAZ Distribuição NF-e. + +> **Note:** This resource uses a separate API host (`api.nfse.io`). You can configure a specific API key with `dataApiKey`, or the SDK will use `apiKey` as fallback. + +**Prerequisites:** +- Company must be registered with a valid A1 digital certificate +- Webhook must be configured to receive NF-e notifications + +#### `enableAutoFetch(companyId: string, options: EnableInboundOptions): Promise` + +Enable automatic NF-e inbound fetching for a company. + +```typescript +// Enable with production environment and webhook v2 +const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + environmentSEFAZ: 'Production', + webhookVersion: '2', +}); + +// Enable starting from a specific NSU +const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + startFromNsu: '999999', + environmentSEFAZ: 'Production', +}); + +// Enable with automatic manifesting +const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + environmentSEFAZ: 'Production', + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, +}); +``` + +**Options:** + +| Property | Type | Description | +|----------|------|-------------| +| `startFromNsu` | `string` | Start searching from this NSU number | +| `startFromDate` | `string` | Start searching from this date (ISO 8601) | +| `environmentSEFAZ` | `string \| null` | SEFAZ environment ('Production', etc.) | +| `automaticManifesting` | `AutomaticManifesting` | Auto-manifest configuration | +| `webhookVersion` | `string` | Webhook version ('1' or '2') | + +#### `disableAutoFetch(companyId: string): Promise` + +Disable automatic NF-e inbound fetching for a company. + +```typescript +const settings = await nfe.inboundProductInvoices.disableAutoFetch('company-id'); +console.log('Status:', settings.status); // 'Inactive' +``` + +#### `getSettings(companyId: string): Promise` + +Get current automatic NF-e inbound settings. + +```typescript +const settings = await nfe.inboundProductInvoices.getSettings('company-id'); +console.log('Status:', settings.status); +console.log('Environment:', settings.environmentSEFAZ); +console.log('Webhook version:', settings.webhookVersion); +console.log('Start NSU:', settings.startFromNsu); +``` + +**Response (`InboundSettings`):** + +| Property | Type | Description | +|----------|------|-------------| +| `status` | `string` | Current status ('Active', 'Inactive', etc.) | +| `startFromNsu` | `string` | Starting NSU number | +| `startFromDate` | `string` | Starting date (if configured) | +| `environmentSEFAZ` | `string \| null` | SEFAZ environment | +| `automaticManifesting` | `AutomaticManifesting` | Auto-manifest configuration | +| `webhookVersion` | `string` | Webhook version | +| `companyId` | `string` | Company ID | +| `createdOn` | `string` | Creation timestamp | +| `modifiedOn` | `string` | Last modification timestamp | + +#### `getDetails(companyId: string, accessKey: string): Promise` + +Retrieve NF-e metadata by 44-digit access key (webhook v1 format). + +```typescript +const nfeDoc = await nfe.inboundProductInvoices.getDetails( + 'company-id', + '35240112345678000190550010000001231234567890' +); +console.log('Issuer:', nfeDoc.issuer?.name); +console.log('Amount:', nfeDoc.totalInvoiceAmount); +console.log('Issued:', nfeDoc.issuedOn); +``` + +#### `getProductInvoiceDetails(companyId: string, accessKey: string): Promise` + +Retrieve NF-e metadata by 44-digit access key (webhook v2 format, recommended). + +```typescript +const nfeDoc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + 'company-id', + '35240112345678000190550010000001231234567890' +); +console.log('Issuer:', nfeDoc.issuer?.name); +console.log('Amount:', nfeDoc.totalInvoiceAmount); +console.log('Product invoices:', nfeDoc.productInvoices?.length); +``` + +**Response (`InboundInvoiceMetadata` / `InboundProductInvoiceMetadata`):** + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `string` | Document ID | +| `accessKey` | `string` | 44-digit access key | +| `nsu` | `string` | NSU number | +| `nfeNumber` | `string` | NF-e number | +| `issuer` | `InboundIssuer` | Issuer information | +| `buyer` | `InboundBuyer` | Buyer information | +| `totalInvoiceAmount` | `string` | Total amount | +| `issuedOn` | `string` | Issue date | +| `description` | `string` | Description | +| `links` | `InboundLinks` | XML/PDF download links | +| `productInvoices` | `InboundProductInvoice[]` | Product invoices (v2 only) | + +#### `getEventDetails(companyId: string, accessKey: string, eventKey: string): Promise` + +Retrieve NF-e event metadata (webhook v1 format). + +```typescript +const event = await nfe.inboundProductInvoices.getEventDetails( + 'company-id', + '35240112345678000190550010000001231234567890', + 'event-key-123' +); +``` + +#### `getProductInvoiceEventDetails(companyId: string, accessKey: string, eventKey: string): Promise` + +Retrieve NF-e event metadata (webhook v2 format). + +```typescript +const event = await nfe.inboundProductInvoices.getProductInvoiceEventDetails( + 'company-id', + '35240112345678000190550010000001231234567890', + 'event-key-123' +); +``` + +#### `getXml(companyId: string, accessKey: string): Promise` + +Download NF-e XML content. + +```typescript +const xml = await nfe.inboundProductInvoices.getXml( + 'company-id', + '35240112345678000190550010000001231234567890' +); +fs.writeFileSync('nfe.xml', xml); +``` + +#### `getEventXml(companyId: string, accessKey: string, eventKey: string): Promise` + +Download NF-e event XML content. + +```typescript +const eventXml = await nfe.inboundProductInvoices.getEventXml( + 'company-id', + '35240112345678000190550010000001231234567890', + 'event-key-123' +); +fs.writeFileSync('nfe-event.xml', eventXml); +``` + +#### `getPdf(companyId: string, accessKey: string): Promise` + +Download NF-e PDF (DANFE). + +```typescript +const pdf = await nfe.inboundProductInvoices.getPdf( + 'company-id', + '35240112345678000190550010000001231234567890' +); +``` + +#### `getJson(companyId: string, accessKey: string): Promise` + +Get NF-e data in JSON format. + +```typescript +const json = await nfe.inboundProductInvoices.getJson( + 'company-id', + '35240112345678000190550010000001231234567890' +); +``` + +#### `manifest(companyId: string, accessKey: string, tpEvent?: ManifestEventType): Promise` + +Send a manifest event for an NF-e. Defaults to `210210` (Ciência da Operação). + +```typescript +// Ciência da Operação (default) +await nfe.inboundProductInvoices.manifest( + 'company-id', + '35240112345678000190550010000001231234567890' +); + +// Confirmação da Operação +await nfe.inboundProductInvoices.manifest( + 'company-id', + '35240112345678000190550010000001231234567890', + 210220 +); + +// Operação não Realizada +await nfe.inboundProductInvoices.manifest( + 'company-id', + '35240112345678000190550010000001231234567890', + 210240 +); +``` + +**Manifest Event Types:** + +| Code | Event | +|------|-------| +| `210210` | Ciência da Operação (awareness, default) | +| `210220` | Confirmação da Operação (confirmation) | +| `210240` | Operação não Realizada (operation not performed) | + +#### `reprocessWebhook(companyId: string, accessKeyOrNsu: string): Promise` + +Reprocess a webhook notification by access key or NSU. + +```typescript +// By access key +await nfe.inboundProductInvoices.reprocessWebhook( + 'company-id', + '35240112345678000190550010000001231234567890' +); + +// By NSU +await nfe.inboundProductInvoices.reprocessWebhook( + 'company-id', + '12345' +); +``` + +--- + ## Types ### Core Types @@ -1922,7 +2173,12 @@ import type { NaturalPerson, Webhook, ListResponse, - PaginationOptions + PaginationOptions, + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings, + EnableInboundOptions, + ManifestEventType } from 'nfe-io'; const config: NfeConfig = { diff --git a/examples/inbound-product-invoices.js b/examples/inbound-product-invoices.js new file mode 100644 index 0000000..9019a50 --- /dev/null +++ b/examples/inbound-product-invoices.js @@ -0,0 +1,258 @@ +/** + * NFE.io SDK v3 - Inbound Product Invoices (NF-e Distribuição) Example + * + * This example demonstrates how to use the Inbound Product Invoices API + * for querying NF-e documents received via Distribuição NF-e (DF-e). + * + * Prerequisites: + * - Company must be registered with a valid A1 digital certificate + * - Webhook must be configured to receive NF-e notifications + * - Valid API key with NF-e distribution access + * + * Configuration: + * Set one of the following environment variables: + * - NFE_DATA_API_KEY - Data/query API key (recommended) + * - NFE_API_KEY - Main API key (will be used as fallback) + * + * Usage: + * node inbound-product-invoices.js [accessKey] + * + * Examples: + * node inbound-product-invoices.js 12345 # Enable and check settings + * node inbound-product-invoices.js 12345 35240... # Retrieve specific NF-e + */ + +import { NfeClient } from 'nfe-io'; + +// ============================================================================ +// Configuration +// ============================================================================ + +const nfe = new NfeClient({ + // dataApiKey: process.env.NFE_DATA_API_KEY, // Uncomment for explicit configuration +}); + +// ============================================================================ +// Example Functions +// ============================================================================ + +/** + * Enable automatic NF-e inbound fetch for a company + */ +async function enableAutoFetch(companyId) { + console.log('\n📡 Enabling automatic NF-e inbound fetch...'); + + try { + const settings = await nfe.inboundProductInvoices.enableAutoFetch(companyId, { + environmentSEFAZ: 'Production', + webhookVersion: '2', + }); + + console.log('✅ Auto-fetch enabled!'); + console.log(' Status:', settings.status); + console.log(' Environment:', settings.environmentSEFAZ); + console.log(' Webhook version:', settings.webhookVersion); + if (settings.startFromNsu) { + console.log(' Starting from NSU:', settings.startFromNsu); + } + return settings; + } catch (error) { + console.error('❌ Failed to enable auto-fetch:', error.message); + throw error; + } +} + +/** + * Get current inbound settings + */ +async function getSettings(companyId) { + console.log('\n⚙️ Fetching inbound settings...'); + + try { + const settings = await nfe.inboundProductInvoices.getSettings(companyId); + console.log('📋 Current settings:'); + console.log(' Status:', settings.status); + console.log(' Environment:', settings.environmentSEFAZ ?? 'Not set'); + console.log(' Webhook version:', settings.webhookVersion); + console.log(' Start from NSU:', settings.startFromNsu ?? 'Not set'); + console.log(' Created:', settings.createdOn); + console.log(' Modified:', settings.modifiedOn); + return settings; + } catch (error) { + console.error('❌ Failed to get settings:', error.message); + throw error; + } +} + +/** + * Get NF-e details by access key (webhook v2 format) + */ +async function getInvoiceDetails(companyId, accessKey) { + console.log('\n🔍 Fetching NF-e details (webhook v2)...'); + + try { + const invoice = await nfe.inboundProductInvoices.getProductInvoiceDetails( + companyId, + accessKey + ); + + console.log('📄 Invoice details:'); + console.log(' Access Key:', invoice.accessKey); + console.log(' NSU:', invoice.nsu); + console.log(' NF-e Number:', invoice.nfeNumber); + console.log(' Issuer:', invoice.issuer?.name); + console.log(' Buyer:', invoice.buyer?.name); + console.log(' Amount:', invoice.totalInvoiceAmount); + console.log(' Issued on:', invoice.issuedOn); + if (invoice.productInvoices?.length) { + console.log(' Product invoices:', invoice.productInvoices.length); + } + return invoice; + } catch (error) { + console.error('❌ Failed to get invoice details:', error.message); + throw error; + } +} + +/** + * Download invoice XML + */ +async function downloadXml(companyId, accessKey) { + console.log('\n📥 Downloading NF-e XML...'); + + try { + const xml = await nfe.inboundProductInvoices.getXml(companyId, accessKey); + console.log('✅ XML downloaded successfully'); + console.log(' Size:', xml.length, 'characters'); + // In a real app, you'd save to file: + // fs.writeFileSync(`nfe-${accessKey}.xml`, xml); + return xml; + } catch (error) { + console.error('❌ Failed to download XML:', error.message); + throw error; + } +} + +/** + * Download invoice PDF (DANFE) + */ +async function downloadPdf(companyId, accessKey) { + console.log('\n📥 Downloading NF-e PDF (DANFE)...'); + + try { + const pdf = await nfe.inboundProductInvoices.getPdf(companyId, accessKey); + console.log('✅ PDF downloaded successfully'); + // In a real app, you'd save to file: + // fs.writeFileSync(`nfe-${accessKey}.pdf`, pdf); + return pdf; + } catch (error) { + console.error('❌ Failed to download PDF:', error.message); + throw error; + } +} + +/** + * Send manifest event (Ciência da Operação by default) + */ +async function sendManifest(companyId, accessKey, tpEvent = 210210) { + const eventNames = { + 210210: 'Ciência da Operação', + 210220: 'Confirmação da Operação', + 210240: 'Operação não Realizada', + }; + const eventName = eventNames[tpEvent] || `Event ${tpEvent}`; + + console.log(`\n📨 Sending manifest: ${eventName}...`); + + try { + const result = await nfe.inboundProductInvoices.manifest( + companyId, + accessKey, + tpEvent + ); + console.log('✅ Manifest sent successfully'); + return result; + } catch (error) { + console.error('❌ Failed to send manifest:', error.message); + throw error; + } +} + +/** + * Reprocess a webhook notification + */ +async function reprocessWebhook(companyId, accessKeyOrNsu) { + console.log('\n🔄 Reprocessing webhook...'); + + try { + const result = await nfe.inboundProductInvoices.reprocessWebhook( + companyId, + accessKeyOrNsu + ); + console.log('✅ Webhook reprocessed successfully'); + return result; + } catch (error) { + console.error('❌ Failed to reprocess webhook:', error.message); + throw error; + } +} + +/** + * Disable automatic NF-e fetching + */ +async function disableAutoFetch(companyId) { + console.log('\n🔌 Disabling auto-fetch...'); + + try { + const settings = await nfe.inboundProductInvoices.disableAutoFetch(companyId); + console.log('✅ Auto-fetch disabled'); + console.log(' Status:', settings.status); + return settings; + } catch (error) { + console.error('❌ Failed to disable auto-fetch:', error.message); + throw error; + } +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + const companyId = process.argv[2]; + const accessKey = process.argv[3]; + + if (!companyId) { + console.error('Usage: node inbound-product-invoices.js [accessKey]'); + process.exit(1); + } + + console.log('🏢 Company ID:', companyId); + if (accessKey) { + console.log('🔑 Access Key:', accessKey); + } + + // Step 1: Enable auto-fetch and check settings + await enableAutoFetch(companyId); + await getSettings(companyId); + + // Step 2: If access key provided, fetch details and downloads + if (accessKey) { + await getInvoiceDetails(companyId, accessKey); + await downloadXml(companyId, accessKey); + await downloadPdf(companyId, accessKey); + + // Step 3: Send manifest (Ciência da Operação) + await sendManifest(companyId, accessKey); + + // Step 4: Reprocess webhook + await reprocessWebhook(companyId, accessKey); + } + + console.log('\n✨ Done!'); +} + +main().catch((error) => { + console.error('\n💥 Unhandled error:', error); + process.exit(1); +}); diff --git a/src/core/client.ts b/src/core/client.ts index ea22610..a7c5aed 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -28,6 +28,7 @@ import { WebhooksResource, AddressesResource, TransportationInvoicesResource, + InboundProductInvoicesResource, ADDRESS_API_BASE_URL } from './resources/index.js'; @@ -135,6 +136,7 @@ export class NfeClient { private _webhooks: WebhooksResource | undefined; private _addresses: AddressesResource | undefined; private _transportationInvoices: TransportationInvoicesResource | undefined; + private _inboundProductInvoices: InboundProductInvoicesResource | undefined; /** * Service Invoices API resource @@ -342,6 +344,51 @@ export class NfeClient { return this._transportationInvoices; } + /** + * Inbound Product Invoices (NF-e Distribution) API resource + * + * @description + * Provides operations for querying NF-e documents received by a company + * via SEFAZ Distribuição DFe: + * - Enable/disable automatic NF-e distribution fetch + * - Retrieve inbound NF-e metadata by access key + * - Download NF-e documents in XML, PDF, and JSON formats + * - Send recipient manifest (Manifestação do Destinatário) + * - Reprocess webhooks + * + * **Prerequisites:** + * - Company must have a valid A1 digital certificate + * - Webhook must be configured to receive NF-e notifications + * + * **Note:** This resource uses a different API host (api.nfse.io). + * Configure `dataApiKey` for a separate key, or it will fallback to `apiKey`. + * + * @see {@link InboundProductInvoicesResource} + * @throws {ConfigurationError} If no API key is configured (dataApiKey or apiKey) + * + * @example + * ```typescript + * // Enable automatic NF-e fetch + * await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + * startFromNsu: '999999', + * environmentSEFAZ: 'Production', + * webhookVersion: '2' + * }); + * + * // Get NF-e details + * const doc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + */ + get inboundProductInvoices(): InboundProductInvoicesResource { + if (!this._inboundProductInvoices) { + this._inboundProductInvoices = new InboundProductInvoicesResource(this.getCteHttpClient()); + } + return this._inboundProductInvoices; + } + /** * Create a new NFE.io API client * diff --git a/src/core/resources/inbound-product-invoices.ts b/src/core/resources/inbound-product-invoices.ts new file mode 100644 index 0000000..8849442 --- /dev/null +++ b/src/core/resources/inbound-product-invoices.ts @@ -0,0 +1,654 @@ +/** + * NFE.io SDK v3 - Inbound Product Invoices Resource + * + * Handles NF-e (Nota Fiscal Eletrônica) distribution queries via Distribuição DFe. + * Uses the API host: api.nfse.io + */ + +import type { HttpClient } from '../http/client.js'; +import type { + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings, + EnableInboundOptions, + ManifestEventType +} from '../types.js'; +import { ValidationError } from '../errors/index.js'; + +// ============================================================================ +// Constants +// ============================================================================ + +/** Regex pattern for valid access key (44 numeric digits) */ +const ACCESS_KEY_PATTERN = /^\d{44}$/; + +/** Default manifest event type: Ciência da Operação */ +const DEFAULT_MANIFEST_EVENT_TYPE: ManifestEventType = 210210; + +// ============================================================================ +// Validation Helpers +// ============================================================================ + +/** + * Validates company ID is not empty + * @param companyId - The company ID to validate + * @throws {ValidationError} If company ID is empty + */ +function validateCompanyId(companyId: string): void { + if (!companyId || companyId.trim() === '') { + throw new ValidationError('Company ID is required'); + } +} + +/** + * Validates access key format (44 numeric digits) + * @param accessKey - The access key to validate + * @throws {ValidationError} If access key format is invalid + */ +function validateAccessKey(accessKey: string): void { + if (!accessKey || accessKey.trim() === '') { + throw new ValidationError('Access key is required'); + } + + const normalized = accessKey.trim(); + if (!ACCESS_KEY_PATTERN.test(normalized)) { + throw new ValidationError( + `Invalid access key: "${accessKey}". Expected 44 numeric digits.` + ); + } +} + +/** + * Validates event key is not empty + * @param eventKey - The event key to validate + * @throws {ValidationError} If event key is empty + */ +function validateEventKey(eventKey: string): void { + if (!eventKey || eventKey.trim() === '') { + throw new ValidationError('Event key is required'); + } +} + +/** + * Validates access key or NSU identifier is not empty + * @param accessKeyOrNsu - The identifier to validate + * @throws {ValidationError} If identifier is empty + */ +function validateAccessKeyOrNsu(accessKeyOrNsu: string): void { + if (!accessKeyOrNsu || accessKeyOrNsu.trim() === '') { + throw new ValidationError('Access key or NSU is required'); + } +} + +// ============================================================================ +// Inbound Product Invoices Resource +// ============================================================================ + +/** + * Inbound Product Invoices (NF-e Distribution) API Resource + * + * @description + * Provides operations for querying NF-e (Nota Fiscal Eletrônica) documents + * received by a company via the SEFAZ Distribuição DFe service. + * + * **Capabilities:** + * - Enable/disable automatic NF-e distribution fetch + * - Retrieve inbound NF-e metadata by access key + * - Download NF-e documents in XML, PDF, and JSON formats + * - Send recipient manifest (Manifestação do Destinatário) + * - Reprocess webhooks + * + * **Prerequisites:** + * - Company must be registered with a valid A1 digital certificate + * - Webhook must be configured to receive NF-e notifications + * + * **Note:** This resource uses a different API host (api.nfse.io) and may require + * a separate API key configured via `dataApiKey` in the client configuration. + * If not set, it falls back to `apiKey`. + * + * @example Enable automatic NF-e search + * ```typescript + * const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + * startFromNsu: '999999', + * environmentSEFAZ: 'Production', + * webhookVersion: '2' + * }); + * ``` + * + * @example Retrieve NF-e details + * ```typescript + * const details = await nfe.inboundProductInvoices.getProductInvoiceDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log(details.nameSender, details.totalInvoiceAmount); + * ``` + * + * @example Download NF-e XML + * ```typescript + * const xml = await nfe.inboundProductInvoices.getXml( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + */ +export class InboundProductInvoicesResource { + private readonly http: HttpClient; + + constructor(http: HttpClient) { + this.http = http; + } + + // -------------------------------------------------------------------------- + // Automatic Search Management + // -------------------------------------------------------------------------- + + /** + * Enable automatic NF-e distribution fetch for a company + * + * Activates the automatic search for NF-e documents destined to the specified + * company via SEFAZ Distribuição DFe. Once enabled, new NF-e documents will be + * automatically retrieved and sent to the configured webhook endpoint. + * + * @param companyId - The company ID to enable automatic search for + * @param options - Configuration options for the automatic search + * @returns Promise with the inbound settings after enabling + * @throws {ValidationError} If company ID is empty + * @throws {BadRequestError} If the request is invalid + * @throws {NotFoundError} If the company is not found + * + * @example + * ```typescript + * const settings = await nfe.inboundProductInvoices.enableAutoFetch('company-id', { + * startFromNsu: '999999', + * startFromDate: '2024-01-01T00:00:00Z', + * environmentSEFAZ: 'Production', + * automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + * webhookVersion: '2' + * }); + * console.log('Status:', settings.status); + * ``` + */ + async enableAutoFetch( + companyId: string, + options: EnableInboundOptions + ): Promise { + validateCompanyId(companyId); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/productinvoices`, + options + ); + + return response.data; + } + + /** + * Disable automatic NF-e distribution fetch for a company + * + * Deactivates the automatic search for NF-e documents. After disabling, + * no new NF-e documents will be retrieved for the company. + * + * @param companyId - The company ID to disable automatic search for + * @returns Promise with the inbound settings after disabling + * @throws {ValidationError} If company ID is empty + * @throws {NotFoundError} If automatic search is not enabled for this company + * + * @example + * ```typescript + * const settings = await nfe.inboundProductInvoices.disableAutoFetch('company-id'); + * console.log('Disabled. Status:', settings.status); + * ``` + */ + async disableAutoFetch(companyId: string): Promise { + validateCompanyId(companyId); + + const response = await this.http.delete( + `/v2/companies/${companyId}/inbound/productinvoices` + ); + + return response.data; + } + + /** + * Get current automatic NF-e distribution fetch settings + * + * Retrieves the current configuration for automatic NF-e search, + * including status, start NSU, start date, and timestamps. + * + * @param companyId - The company ID to get settings for + * @returns Promise with the current inbound settings + * @throws {ValidationError} If company ID is empty + * @throws {NotFoundError} If automatic search is not configured for this company + * + * @example + * ```typescript + * const settings = await nfe.inboundProductInvoices.getSettings('company-id'); + * console.log('Status:', settings.status); + * console.log('Start NSU:', settings.startFromNsu); + * console.log('Webhook version:', settings.webhookVersion); + * ``` + */ + async getSettings(companyId: string): Promise { + validateCompanyId(companyId); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoices` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Document Detail Operations + // -------------------------------------------------------------------------- + + /** + * Get details of an inbound NF-e/CT-e by access key (webhook v1 format) + * + * Retrieves the metadata of an inbound document using its 44-digit access key. + * This is the generic endpoint that works for both NF-e and CT-e documents. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the inbound invoice metadata + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const doc = await nfe.inboundProductInvoices.getDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log('Sender:', doc.nameSender); + * console.log('Amount:', doc.totalInvoiceAmount); + * console.log('NSU:', doc.nsu); + * ``` + */ + async getDetails( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}` + ); + + return response.data; + } + + /** + * Get details of an inbound NF-e by access key (webhook v2 format) + * + * Retrieves the metadata of an NF-e document using its 44-digit access key. + * This endpoint returns additional `productInvoices` array compared to the v1 format. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the inbound product invoice metadata (includes productInvoices array) + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const doc = await nfe.inboundProductInvoices.getProductInvoiceDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log('Sender:', doc.nameSender); + * console.log('Product invoices:', doc.productInvoices.length); + * ``` + */ + async getProductInvoiceDetails( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKey.trim()}` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Event Detail Operations + // -------------------------------------------------------------------------- + + /** + * Get details of an event related to an inbound NF-e/CT-e (generic endpoint) + * + * Retrieves the metadata of an event associated with an inbound document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key of the parent document + * @param eventKey - The event key + * @returns Promise with the event metadata + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const event = await nfe.inboundProductInvoices.getEventDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 'event-key-123' + * ); + * console.log('Event:', event.description); + * ``` + */ + async getEventDetails( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + validateEventKey(eventKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/events/${eventKey.trim()}` + ); + + return response.data; + } + + /** + * Get details of an event related to an inbound NF-e (product invoice endpoint) + * + * Retrieves the metadata of an event associated with an inbound NF-e document. + * Returns the webhook v2 format with `productInvoices` array. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key of the parent document + * @param eventKey - The event key + * @returns Promise with the product invoice event metadata + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const event = await nfe.inboundProductInvoices.getProductInvoiceEventDetails( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 'event-key-123' + * ); + * console.log('Event:', event.description); + * console.log('Product invoices:', event.productInvoices.length); + * ``` + */ + async getProductInvoiceEventDetails( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + validateEventKey(eventKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKey.trim()}/events/${eventKey.trim()}` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // File Download Operations + // -------------------------------------------------------------------------- + + /** + * Download XML of an inbound NF-e/CT-e by access key + * + * Gets the XML content of an inbound document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the XML content as a string + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const xml = await nfe.inboundProductInvoices.getXml( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('nfe.xml', xml); + * ``` + */ + async getXml(companyId: string, accessKey: string): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/xml` + ); + + return response.data; + } + + /** + * Download XML of an event related to an inbound NF-e/CT-e + * + * Gets the XML content of an event associated with an inbound document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key of the parent document + * @param eventKey - The event key + * @returns Promise with the event XML content as a string + * @throws {ValidationError} If any parameter is invalid + * @throws {NotFoundError} If the event is not found + * + * @example + * ```typescript + * const xml = await nfe.inboundProductInvoices.getEventXml( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 'event-key-123' + * ); + * fs.writeFileSync('nfe-event.xml', xml); + * ``` + */ + async getEventXml( + companyId: string, + accessKey: string, + eventKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + validateEventKey(eventKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/events/${eventKey.trim()}/xml` + ); + + return response.data; + } + + /** + * Download PDF of an inbound NF-e by access key + * + * Gets the PDF content of an NF-e document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the PDF content as a string + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const pdf = await nfe.inboundProductInvoices.getPdf( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * fs.writeFileSync('nfe.pdf', pdf); + * ``` + */ + async getPdf(companyId: string, accessKey: string): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/pdf` + ); + + return response.data; + } + + /** + * Get JSON representation of an inbound NF-e by access key + * + * Gets the structured JSON data of an NF-e document. + * + * @param companyId - The company ID that received the document + * @param accessKey - The 44-digit access key + * @returns Promise with the NF-e metadata in JSON format + * @throws {ValidationError} If company ID or access key is invalid + * @throws {NotFoundError} If the document is not found + * + * @example + * ```typescript + * const data = await nfe.inboundProductInvoices.getJson( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * console.log('Sender:', data.nameSender); + * console.log('Amount:', data.totalInvoiceAmount); + * ``` + */ + async getJson( + companyId: string, + accessKey: string + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.get( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKey.trim()}/json` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Manifest Operations + // -------------------------------------------------------------------------- + + /** + * Send recipient manifest (Manifestação do Destinatário) for an NF-e + * + * Sends a manifest event for an NF-e document identified by its access key. + * Defaults to "Ciência da Operação" (210210) if no event type is specified. + * + * **Event types:** + * - `210210` — Ciência da Operação (awareness, default) + * - `210220` — Confirmação da Operação (confirmation) + * - `210240` — Operação não Realizada (operation not performed) + * + * @param companyId - The company ID + * @param accessKey - The 44-digit access key of the NF-e + * @param tpEvent - Manifest event type (defaults to 210210) + * @returns Promise with the manifest response + * @throws {ValidationError} If company ID or access key is invalid + * + * @example Default manifest (Ciência da Operação) + * ```typescript + * const result = await nfe.inboundProductInvoices.manifest( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + * + * @example Confirm operation + * ```typescript + * const result = await nfe.inboundProductInvoices.manifest( + * 'company-id', + * '35240112345678000190550010000001231234567890', + * 210220 + * ); + * ``` + */ + async manifest( + companyId: string, + accessKey: string, + tpEvent: ManifestEventType = DEFAULT_MANIFEST_EVENT_TYPE + ): Promise { + validateCompanyId(companyId); + validateAccessKey(accessKey); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/${accessKey.trim()}/manifest?tpEvent=${tpEvent}` + ); + + return response.data; + } + + // -------------------------------------------------------------------------- + // Webhook Operations + // -------------------------------------------------------------------------- + + /** + * Reprocess webhook for an inbound NF-e by access key or NSU + * + * Triggers reprocessing of the webhook notification for a specific document, + * identified either by its 44-digit access key or by its NSU number. + * + * @param companyId - The company ID + * @param accessKeyOrNsu - The 44-digit access key or NSU number + * @returns Promise with the product invoice metadata + * @throws {ValidationError} If company ID or identifier is empty + * @throws {NotFoundError} If the document is not found + * + * @example Reprocess by access key + * ```typescript + * const result = await nfe.inboundProductInvoices.reprocessWebhook( + * 'company-id', + * '35240112345678000190550010000001231234567890' + * ); + * ``` + * + * @example Reprocess by NSU + * ```typescript + * const result = await nfe.inboundProductInvoices.reprocessWebhook( + * 'company-id', + * '12345' + * ); + * ``` + */ + async reprocessWebhook( + companyId: string, + accessKeyOrNsu: string + ): Promise { + validateCompanyId(companyId); + validateAccessKeyOrNsu(accessKeyOrNsu); + + const response = await this.http.post( + `/v2/companies/${companyId}/inbound/productinvoice/${accessKeyOrNsu.trim()}/processwebhook` + ); + + return response.data; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Creates an InboundProductInvoicesResource instance + * + * @param http - HTTP client configured for the inbound API (api.nfse.io) + * @returns InboundProductInvoicesResource instance + */ +export function createInboundProductInvoicesResource( + http: HttpClient +): InboundProductInvoicesResource { + return new InboundProductInvoicesResource(http); +} diff --git a/src/core/resources/index.ts b/src/core/resources/index.ts index 6f89faa..849751d 100644 --- a/src/core/resources/index.ts +++ b/src/core/resources/index.ts @@ -12,3 +12,4 @@ export { NaturalPeopleResource } from './natural-people.js'; export { WebhooksResource } from './webhooks.js'; export { AddressesResource, createAddressesResource, ADDRESS_API_BASE_URL } from './addresses.js'; export { TransportationInvoicesResource, createTransportationInvoicesResource, CTE_API_BASE_URL } from './transportation-invoices.js'; +export { InboundProductInvoicesResource, createInboundProductInvoicesResource } from './inbound-product-invoices.js'; diff --git a/src/core/types.ts b/src/core/types.ts index bf3f18b..9a8430c 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -437,3 +437,186 @@ export type TransportationInvoiceEntityStatus = */ export type TransportationInvoiceMetadataType = CteComponents['schemas']['DFe.NetCore.Domain.Enums.MetadataResourceType']; + +// ============================================================================ +// Inbound NF-e Distribution Types +// ============================================================================ + +/** + * Company reference in inbound document metadata + */ +export interface InboundCompany { + /** Company ID */ + id: string; + /** Company CNPJ */ + federalTaxNumber: string; +} + +/** + * Issuer reference in inbound document metadata + */ +export interface InboundIssuer { + /** Issuer CNPJ */ + federalTaxNumber: string; + /** Issuer name */ + name: string; +} + +/** + * Buyer reference in inbound document metadata + */ +export interface InboundBuyer { + /** Buyer CNPJ/CPF */ + federalTaxNumber: string; + /** Buyer name */ + name: string; +} + +/** + * Transportation entity reference in inbound document metadata + */ +export interface InboundTransportation { + /** Transportation CNPJ */ + federalTaxNumber: string; + /** Transportation name */ + name: string; +} + +/** + * Document download links + */ +export interface InboundLinks { + /** XML download URL */ + xml: string; + /** PDF download URL */ + pdf: string; +} + +/** + * Product invoice reference (used in webhook v2 responses) + */ +export interface InboundProductInvoice { + /** Access key of the referenced product invoice */ + accessKey: string; +} + +/** + * Automatic manifesting configuration + */ +export interface AutomaticManifesting { + /** Minutes to wait before automatic awareness operation */ + minutesToWaitAwarenessOperation: string; +} + +/** + * Inbound invoice metadata (webhook v1 format) + * + * Contains details of an NF-e or CT-e document retrieved via Distribuição DFe. + * Corresponds to the generic endpoint `GET /{access_key}`. + */ +export interface InboundInvoiceMetadata { + /** Document ID */ + id: string; + /** Creation timestamp */ + createdOn: string; + /** 44-digit access key */ + accessKey: string; + /** Parent document access key (for events) */ + parentAccessKey: string; + /** Company that received the document */ + company: InboundCompany; + /** Document issuer */ + issuer: InboundIssuer; + /** Document buyer */ + buyer: InboundBuyer; + /** Transportation entity */ + transportation: InboundTransportation; + /** Download links */ + links: InboundLinks; + /** XML download URL */ + xmlUrl: string; + /** Sender CNPJ */ + federalTaxNumberSender: string; + /** Sender name */ + nameSender: string; + /** Document type */ + type: string | null; + /** NSU (Número Sequencial Único) */ + nsu: string; + /** Parent NSU */ + nsuParent: string; + /** NF-e number */ + nfeNumber: string; + /** NF-e serial number */ + nfeSerialNumber: string; + /** Issue date */ + issuedOn: string; + /** Document description */ + description: string; + /** Total invoice amount */ + totalInvoiceAmount: string; + /** Operation type */ + operationType: string | null; +} + +/** + * Inbound product invoice metadata (webhook v2 format) + * + * Extends the base metadata with product invoice references. + * Corresponds to the `GET /productinvoice/{access_key}` endpoint. + */ +export interface InboundProductInvoiceMetadata extends Omit { + /** Referenced product invoices */ + productInvoices: InboundProductInvoice[]; +} + +/** + * Inbound NF-e distribution service settings + * + * Configuration for automatic NF-e search via SEFAZ Distribuição DFe. + */ +export interface InboundSettings { + /** Starting NSU for document retrieval */ + startFromNsu: string; + /** Starting date for document retrieval */ + startFromDate: string; + /** SEFAZ environment (e.g., Production) */ + environmentSEFAZ: string | null; + /** Automatic manifesting configuration */ + automaticManifesting: AutomaticManifesting; + /** Webhook version */ + webhookVersion: string; + /** Company ID */ + companyId: string; + /** Service status */ + status: string | null; + /** Creation timestamp */ + createdOn: string; + /** Last modification timestamp */ + modifiedOn: string; +} + +/** + * Options for enabling automatic NF-e distribution fetch + */ +export interface EnableInboundOptions { + /** Starting NSU number */ + startFromNsu?: string; + /** Starting date (ISO 8601 format) */ + startFromDate?: string; + /** SEFAZ environment */ + environmentSEFAZ?: string; + /** Automatic manifesting settings */ + automaticManifesting?: AutomaticManifesting; + /** Webhook version */ + webhookVersion?: string; +} + +/** + * Manifest event types for Manifestação do Destinatário + * + * - `210210` — Ciência da Operação (awareness of the operation) + * - `210220` — Confirmação da Operação (confirmation of the operation) + * - `210240` — Operação não Realizada (operation not performed) + */ +export type ManifestEventType = 210210 | 210220 | 210240; diff --git a/src/generated/calculo-impostos-v1.ts b/src/generated/calculo-impostos-v1.ts index 26a4735..5e58ca1 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-14T00:32:45.806Z + * Last generated: 2026-02-14T03:25:58.481Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-cte-v2.ts b/src/generated/consulta-cte-v2.ts index bbf5638..5a8de0d 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-14T00:32:45.826Z + * Last generated: 2026-02-14T03:25:58.500Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-nfe-distribuicao-v1.ts b/src/generated/consulta-nfe-distribuicao-v1.ts index 9afc65b..b273206 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-14T00:32:45.857Z + * Last generated: 2026-02-14T03:25:58.534Z * Generator: openapi-typescript */ diff --git a/src/generated/index.ts b/src/generated/index.ts index 393bb48..794bca1 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-14T00:32:46.103Z + * Last updated: 2026-02-14T03:25:58.777Z */ // ============================================================================ diff --git a/src/generated/nf-consumidor-v2.ts b/src/generated/nf-consumidor-v2.ts index dc2bb30..26229f8 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-14T00:32:45.948Z + * Last generated: 2026-02-14T03:25:58.627Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-produto-v2.ts b/src/generated/nf-produto-v2.ts index e32e771..9e6a365 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-14T00:32:46.031Z + * Last generated: 2026-02-14T03:25:58.708Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-servico-v1.ts b/src/generated/nf-servico-v1.ts index 99a2faf..c5999ad 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-14T00:32:46.092Z + * Last generated: 2026-02-14T03:25:58.768Z * Generator: openapi-typescript */ diff --git a/src/generated/nfeio.ts b/src/generated/nfeio.ts index e42a71b..c060623 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-14T00:32:46.102Z + * Last generated: 2026-02-14T03:25:58.776Z * Generator: openapi-typescript */ diff --git a/src/index.ts b/src/index.ts index f3daaf2..8e3b75b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,6 +88,20 @@ export type { TransportationInvoiceEntityStatus, TransportationInvoiceMetadataType, + // Inbound NF-e Distribution types + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings, + EnableInboundOptions, + ManifestEventType, + InboundCompany, + InboundIssuer, + InboundBuyer, + InboundTransportation, + InboundLinks, + InboundProductInvoice, + AutomaticManifesting, + // Common types EntityType, TaxRegime, diff --git a/tests/unit/resources/inbound-product-invoices.test.ts b/tests/unit/resources/inbound-product-invoices.test.ts new file mode 100644 index 0000000..ed43297 --- /dev/null +++ b/tests/unit/resources/inbound-product-invoices.test.ts @@ -0,0 +1,510 @@ +/** + * Unit tests for InboundProductInvoicesResource + * Tests NF-e distribution (Consulta NF-e Distribuição) operations + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { InboundProductInvoicesResource } from '../../../src/core/resources/inbound-product-invoices.js'; +import { HttpClient } from '../../../src/core/http/client.js'; +import type { + HttpResponse, + InboundInvoiceMetadata, + InboundProductInvoiceMetadata, + InboundSettings +} from '../../../src/core/types.js'; +import { ValidationError } from '../../../src/core/errors/index.js'; + +describe('InboundProductInvoicesResource', () => { + let resource: InboundProductInvoicesResource; + let mockHttpClient: { + get: ReturnType; + post: ReturnType; + put: ReturnType; + delete: ReturnType; + }; + + // Valid 44-digit access key for testing + const validAccessKey = '35240112345678000190550010000001231234567890'; + const testCompanyId = 'company-123'; + const testEventKey = 'event-key-456'; + + beforeEach(() => { + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + resource = new InboundProductInvoicesResource(mockHttpClient as unknown as HttpClient); + }); + + // ========================================================================== + // Validation tests + // ========================================================================== + + describe('validation', () => { + it('should throw ValidationError for empty companyId on enableAutoFetch', async () => { + await expect(resource.enableAutoFetch('', {})).rejects.toThrow(ValidationError); + await expect(resource.enableAutoFetch('', {})).rejects.toThrow(/Company ID is required/); + }); + + it('should throw ValidationError for whitespace-only companyId', async () => { + await expect(resource.enableAutoFetch(' ', {})).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty companyId on disableAutoFetch', async () => { + await expect(resource.disableAutoFetch('')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty companyId on getSettings', async () => { + await expect(resource.getSettings('')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for invalid access key format', async () => { + await expect(resource.getDetails(testCompanyId, '12345')).rejects.toThrow(ValidationError); + await expect(resource.getDetails(testCompanyId, '12345')).rejects.toThrow(/Expected 44 numeric digits/); + }); + + it('should throw ValidationError for empty access key', async () => { + await expect(resource.getDetails(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.getDetails(testCompanyId, '')).rejects.toThrow(/Access key is required/); + }); + + it('should throw ValidationError for non-numeric access key', async () => { + await expect(resource.getDetails(testCompanyId, 'a'.repeat(44))).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty eventKey', async () => { + await expect(resource.getEventDetails(testCompanyId, validAccessKey, '')).rejects.toThrow(ValidationError); + await expect(resource.getEventDetails(testCompanyId, validAccessKey, '')).rejects.toThrow(/Event key is required/); + }); + + it('should throw ValidationError for whitespace-only eventKey', async () => { + await expect(resource.getEventDetails(testCompanyId, validAccessKey, ' ')).rejects.toThrow(ValidationError); + }); + + it('should throw ValidationError for empty accessKeyOrNsu on reprocessWebhook', async () => { + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(/Access key or NSU is required/); + }); + }); + + // ========================================================================== + // enableAutoFetch() tests + // ========================================================================== + + describe('enableAutoFetch', () => { + const mockSettings: InboundSettings = { + startFromNsu: '999999', + startFromDate: '2024-01-01T00:00:00Z', + environmentSEFAZ: 'Production', + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + webhookVersion: '2', + companyId: testCompanyId, + status: 'Active', + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-15T10:30:00Z', + }; + + it('should enable auto-fetch with provided options', async () => { + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const options = { + startFromNsu: '999999', + environmentSEFAZ: 'Production', + webhookVersion: '2', + }; + + const result = await resource.enableAutoFetch(testCompanyId, options); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoices`, + options + ); + }); + }); + + // ========================================================================== + // disableAutoFetch() tests + // ========================================================================== + + describe('disableAutoFetch', () => { + it('should disable auto-fetch for a company', async () => { + const mockSettings: InboundSettings = { + startFromNsu: '999999', + startFromDate: '2024-01-01T00:00:00Z', + environmentSEFAZ: null, + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + webhookVersion: '2', + companyId: testCompanyId, + status: 'Inactive', + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-16T10:30:00Z', + }; + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.delete.mockResolvedValue(mockResponse); + + const result = await resource.disableAutoFetch(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.delete).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoices` + ); + }); + }); + + // ========================================================================== + // getSettings() tests + // ========================================================================== + + describe('getSettings', () => { + it('should get current settings for a company', async () => { + const mockSettings: InboundSettings = { + startFromNsu: '999999', + startFromDate: '2024-01-01T00:00:00Z', + environmentSEFAZ: 'Production', + automaticManifesting: { minutesToWaitAwarenessOperation: '30' }, + webhookVersion: '2', + companyId: testCompanyId, + status: 'Active', + createdOn: '2024-01-15T10:30:00Z', + modifiedOn: '2024-01-15T10:30:00Z', + }; + const mockResponse: HttpResponse = { + data: mockSettings, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getSettings(testCompanyId); + + expect(result).toEqual(mockSettings); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoices` + ); + }); + }); + + // ========================================================================== + // getDetails() / getProductInvoiceDetails() tests + // ========================================================================== + + describe('getDetails', () => { + const mockMetadata: InboundInvoiceMetadata = { + id: 'doc-123', + createdOn: '2024-01-15T10:30:00Z', + accessKey: validAccessKey, + parentAccessKey: '', + company: { id: testCompanyId, federalTaxNumber: '12345678000190' }, + issuer: { federalTaxNumber: '98765432000110', name: 'Issuer Corp' }, + buyer: { federalTaxNumber: '12345678000190', name: 'Buyer Corp' }, + transportation: { federalTaxNumber: '11111111000111', name: 'Transport Co' }, + links: { xml: 'https://example.com/xml', pdf: 'https://example.com/pdf' }, + xmlUrl: 'https://example.com/xml', + federalTaxNumberSender: '98765432000110', + nameSender: 'Issuer Corp', + type: null, + nsu: '12345', + nsuParent: '', + nfeNumber: '1001', + nfeSerialNumber: '1', + issuedOn: '2024-01-10T08:00:00Z', + description: 'Test invoice', + totalInvoiceAmount: '1500.00', + operationType: null, + }; + + it('should get details by access key (webhook v1)', async () => { + const mockResponse: HttpResponse = { + data: mockMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getDetails(testCompanyId, validAccessKey); + + expect(result).toEqual(mockMetadata); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}` + ); + }); + }); + + describe('getProductInvoiceDetails', () => { + const mockProductMetadata: InboundProductInvoiceMetadata = { + id: 'doc-123', + createdOn: '2024-01-15T10:30:00Z', + accessKey: validAccessKey, + parentAccessKey: '', + company: { id: testCompanyId, federalTaxNumber: '12345678000190' }, + issuer: { federalTaxNumber: '98765432000110', name: 'Issuer Corp' }, + buyer: { federalTaxNumber: '12345678000190', name: 'Buyer Corp' }, + transportation: { federalTaxNumber: '11111111000111', name: 'Transport Co' }, + links: { xml: 'https://example.com/xml', pdf: 'https://example.com/pdf' }, + xmlUrl: 'https://example.com/xml', + federalTaxNumberSender: '98765432000110', + nameSender: 'Issuer Corp', + type: null, + nsu: '12345', + nfeNumber: '1001', + issuedOn: '2024-01-10T08:00:00Z', + description: 'Test invoice', + totalInvoiceAmount: '1500.00', + productInvoices: [{ accessKey: '11111111111111111111111111111111111111111111' }], + }; + + it('should get product invoice details by access key (webhook v2)', async () => { + const mockResponse: HttpResponse = { + data: mockProductMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getProductInvoiceDetails(testCompanyId, validAccessKey); + + expect(result).toEqual(mockProductMetadata); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}` + ); + }); + }); + + // ========================================================================== + // getEventDetails() / getProductInvoiceEventDetails() tests + // ========================================================================== + + describe('getEventDetails', () => { + it('should get event details with correct path', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getEventDetails(testCompanyId, validAccessKey, testEventKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}` + ); + }); + + it('should throw ValidationError for empty eventKey', async () => { + await expect( + resource.getEventDetails(testCompanyId, validAccessKey, '') + ).rejects.toThrow(ValidationError); + }); + }); + + describe('getProductInvoiceEventDetails', () => { + it('should get product invoice event details with correct path', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundProductInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getProductInvoiceEventDetails(testCompanyId, validAccessKey, testEventKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}/events/${testEventKey}` + ); + }); + }); + + // ========================================================================== + // Download methods tests (getXml, getEventXml, getPdf, getJson) + // ========================================================================== + + describe('getXml', () => { + it('should download XML with correct path', async () => { + const mockResponse: HttpResponse = { + data: 'content', + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getXml(testCompanyId, validAccessKey); + + expect(result).toBe('content'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/xml` + ); + }); + }); + + describe('getEventXml', () => { + it('should download event XML with correct path', async () => { + const mockResponse: HttpResponse = { + data: 'event', + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getEventXml(testCompanyId, validAccessKey, testEventKey); + + expect(result).toBe('event'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/events/${testEventKey}/xml` + ); + }); + + it('should throw ValidationError for empty eventKey', async () => { + await expect( + resource.getEventXml(testCompanyId, validAccessKey, '') + ).rejects.toThrow(ValidationError); + }); + }); + + describe('getPdf', () => { + it('should download PDF with correct path', async () => { + const mockResponse: HttpResponse = { + data: 'pdf-content', + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + const result = await resource.getPdf(testCompanyId, validAccessKey); + + expect(result).toBe('pdf-content'); + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/pdf` + ); + }); + }); + + describe('getJson', () => { + it('should get JSON with correct path', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.get.mockResolvedValue(mockResponse); + + await resource.getJson(testCompanyId, validAccessKey); + + expect(mockHttpClient.get).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}/json` + ); + }); + }); + + // ========================================================================== + // manifest() tests + // ========================================================================== + + describe('manifest', () => { + it('should send manifest with default tpEvent=210210', async () => { + const mockResponse: HttpResponse = { + data: 'ok', + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + const result = await resource.manifest(testCompanyId, validAccessKey); + + expect(result).toBe('ok'); + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/manifest?tpEvent=210210` + ); + }); + + it('should send manifest with explicit tpEvent=210220', async () => { + const mockResponse: HttpResponse = { + data: 'ok', + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.manifest(testCompanyId, validAccessKey, 210220); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/manifest?tpEvent=210220` + ); + }); + + it('should send manifest with tpEvent=210240', async () => { + const mockResponse: HttpResponse = { + data: 'ok', + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.manifest(testCompanyId, validAccessKey, 210240); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/${validAccessKey}/manifest?tpEvent=210240` + ); + }); + + it('should throw ValidationError for invalid access key', async () => { + await expect(resource.manifest(testCompanyId, 'invalid')).rejects.toThrow(ValidationError); + }); + }); + + // ========================================================================== + // reprocessWebhook() tests + // ========================================================================== + + describe('reprocessWebhook', () => { + it('should reprocess webhook by access key', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundProductInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.reprocessWebhook(testCompanyId, validAccessKey); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/${validAccessKey}/processwebhook` + ); + }); + + it('should reprocess webhook by NSU', async () => { + const mockResponse: HttpResponse = { + data: {} as InboundProductInvoiceMetadata, + status: 200, + headers: {}, + }; + mockHttpClient.post.mockResolvedValue(mockResponse); + + await resource.reprocessWebhook(testCompanyId, '12345'); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + `/v2/companies/${testCompanyId}/inbound/productinvoice/12345/processwebhook` + ); + }); + + it('should throw ValidationError for empty identifier', async () => { + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(ValidationError); + await expect(resource.reprocessWebhook(testCompanyId, '')).rejects.toThrow(/Access key or NSU is required/); + }); + + it('should throw ValidationError for whitespace-only identifier', async () => { + await expect(resource.reprocessWebhook(testCompanyId, ' ')).rejects.toThrow(ValidationError); + }); + }); +});