Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,90 @@ Todas as mudanças notáveis neste projeto serão documentadas neste arquivo.
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/),
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).

## [5.2.0] - 2026-07-13

> Correção do contrato de paginação de `companies` contra a API real, provado
> por sonda ao vivo (2026-07-13, duas contas). O request de `GET /companies` é
> **1-based** — a API rejeita `pageIndex: 0` com `"pageIndex must be greater or
> equal to 1"` — igual ao de `serviceInvoices` (rejeição de `pageIndex: 0`
> reconfirmada ao vivo), não o oposto como se supunha.

### Corrigido

- **`companies.listAll()` e `companies.listIterator()` falhavam na primeira
chamada**, por dois motivos independentes: (1) iniciavam a paginação em
`pageIndex = 0`, que a API rejeita — agora iniciam em `1`; (2) pediam
`pageCount: 100`, mas a API limita `GET /companies` a **50 itens por página**
(sonda ao vivo: aceita 2–50; rejeita 1, apesar da mensagem `"between 1 and
50"`, e rejeita ≥51) — agora pedem `50`. Consertados transitivamente os
quatro métodos que dependem de `listAll()`: `findByTaxNumber`, `findByName`,
`getCompaniesWithCertificates` e `getCompaniesWithExpiringCertificates`.
- `companies.list()` ecoava `pageCount: 100` como default no `page` da resposta;
o default real da API é **10 itens** quando `pageCount` é omitido.
- ⚠️ **JSDoc de `companies.update()` era ativamente enganoso**: dizia *"only
fields to update"* (semântica PATCH), mas a API faz **PUT (substituição
total)** — update parcial seguindo o docblock resulta em 400 ou em campos
zerados silenciosamente. O JSDoc agora documenta o replace integral com
exemplo read-modify-write. As assinaturas frouxas (`Partial<Company>`/
`Omit<Company>`) foram mantidas por compat; o aperto para os schemas
estritos fica para a próxima major (política de versionamento) ou para a
migração de CRUD v2.
- JSDoc de `companies.create()` documenta os obrigatórios reais do corpo
(`name`, `federalTaxNumber`, `taxRegime`, `address`) e que `email` **não**
faz parte dele; exemplo corrigido (o anterior compilava e falhava com 400).
- Vitest coletava os testes **5×** através dos symlinks `client-php`/
`client-ruby` (que apontam de volta para este repo), causando flakes por
corrida no `.test-temp` compartilhado e inflando a suíte (3710 → 698 testes
reais). O `include` agora é restrito a `tests/**`.

### Deprecado

- **`companies.list()`** (API v1, paginação offset): a API v1 de companies
está sendo descontinuada. Use `listV2()` (cursor, v2) para listagem
paginada, ou `listAll()`/`listIterator()` para varredura completa. O método
continua funcionando (e corrigido — ver acima) durante a convivência.
**Nota**: `listAll()`/`listIterator()` permanecem no transporte v1 nesta
release porque a enumeração completa via v2 falha de forma determinística
em contas com certos registros (HTTP 500 server-side em qualquer janela
que os contenha — bug reportado ao backend em 2026-07-14), e porque as
projeções v1/v2 divergem de campos. A troca de transporte fica para quando
o backend corrigir o 500.
- JSDoc de `companies.list` e `serviceInvoices.list`, `docs/API.md` e `README`
exemplificavam `pageIndex: 0` — todos corrigidos para a convenção 1-based.
- `PaginationOptions.pageIndex` e `PageInfo.pageIndex` documentados como
1-based (a primeira página é 1).
- Specs OpenAPI (`nf-servico-v1.yaml`): `pageIndex` agora declara `minimum: 1`
em `/v1/companies` e na listagem de notas de serviço; `pageCount` de
`/v1/companies` declara `maximum: 50`.

### Alterado

- ⚠️ **Nota de migração**: `companies.list()` deixou de subtrair 1 da resposta.
`list({ pageIndex: 1 }).page.pageIndex` agora retorna `1` (antes retornava
`0`). A convenção do SDK passa a ser **1-based nos dois lados** (request e
response), fiel ao fio da API. Se seu código lia `page.pageIndex` assumindo
base 0 (ex.: `pageIndex + 1` para exibir o número da página), remova o ajuste.

### Adicionado

- **`companies.listV2()`** — listagem pela API v2 (`api.nfse.io/v2/companies`,
contribuintes-v2), **cursor-based**: `{ limit (1–50, default 10),
startingAfter, endingBefore }` → `{ data, hasMore }` (contrato provado ao
vivo em 2026-07-14). Os itens seguem a projeção v2 (`CompanyResourceItem`)
— shape diferente do `Company` v1 (sem campos de configuração NFS-e; com
`stateTaxes`, `municipalTaxes`, `type`, `version`). `limit` fora de 1–50 é
rejeitado client-side (a API aceitaria `limit: 0` devolvendo página vazia).
Tipos novos: `CompanyV2ListOptions`, `CompanyV2ListResponse`.
- Testes de contrato de paginação: mock espelha a rejeição da API a
`pageIndex: 0` e trava a regressão — `listAll`/`listIterator` são testados
iniciando em 1 e incrementando 1 → 2; suite equivalente para o contrato
cursor do `listV2`.
- Teste de alinhamento de tipos para os corpos de escrita de companies
(`tests/types/company-write-alignment.test-d.ts`): pina os obrigatórios de
`CreateCompanyResourceItem`/`UpdateCompanyResourceItem` (mesmo conjunto —
evidência da semântica PUT) e a ausência de `email` no corpo; um sync de
spec que mude o contrato quebra o `npm run test:types` em vez de driftar.

## [5.1.0] - 2026-07-03

> Correção do contrato de webhooks contra a API real, provado por sonda ao vivo
Expand Down Expand Up @@ -837,7 +921,8 @@ SDK JavaScript legado com API baseada em callbacks.

## Links

[Unreleased]: https://github.com/nfe/client-nodejs/compare/v5.1.0...HEAD
[Unreleased]: https://github.com/nfe/client-nodejs/compare/v5.2.0...HEAD
[5.2.0]: https://github.com/nfe/client-nodejs/compare/v5.1.0...v5.2.0
[5.1.0]: https://github.com/nfe/client-nodejs/compare/v5.0.0...v5.1.0
[5.0.0]: https://github.com/nfe/client-nodejs/releases/tag/v5.0.0
[3.0.0]: https://github.com/nfe/client-nodejs/releases/tag/v3.0.0
Expand Down
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ if ('id' in result) {
// Listar notas fiscais com filtros
const notas = await nfe.serviceInvoices.list(empresaId, {
pageCount: 50,
pageIndex: 0,
pageIndex: 1, // paginação é 1-based (primeira página = 1)
searchPeriod: {
startDate: '2024-01-01',
endDate: '2024-01-31',
Expand Down Expand Up @@ -259,22 +259,38 @@ console.log(`✅ ${notas.length} notas fiscais criadas em lote`);
Gerenciar empresas na sua conta:

```typescript
// Criar empresa
// Criar empresa (a API exige name, federalTaxNumber, taxRegime e address)
const empresa = await nfe.companies.create({
federalTaxNumber: '12345678000190',
name: 'Nome da Empresa',
// ... outros campos
federalTaxNumber: 12345678000190,
taxRegime: 'SimplesNacional',
address: {
state: 'SP',
city: { code: '3550308', name: 'São Paulo' },
district: 'Centro',
street: 'Rua Exemplo',
number: '100',
postalCode: '01001000',
country: 'BRA',
},
});

// Listar todas as empresas
const empresas = await nfe.companies.list();
// Listar empresas — v2, cursor-based (recomendado; a API v1 está sendo descontinuada)
const pagina = await nfe.companies.listV2({ limit: 50 });
// próxima página: listV2({ limit: 50, startingAfter: <id do último item> })

// Listar todas as empresas (varredura completa com paginação automática)
const empresas = await nfe.companies.listAll();

// Buscar empresa específica
const empresa = await nfe.companies.retrieve(empresaId);

// Atualizar empresa
// Atualizar empresa — ATENÇÃO: é PUT (substituição total), não update parcial.
// Envie o objeto completo (read-modify-write); campos omitidos são zerados.
const atual = await nfe.companies.retrieve(empresaId);
const atualizada = await nfe.companies.update(empresaId, {
email: 'novoemail@empresa.com.br'
...atual,
tradeName: 'Novo Nome Fantasia',
});

// Upload de certificado digital
Expand Down
41 changes: 33 additions & 8 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ List service invoices for a company with pagination and filtering.
| Option | Type | Description |
|--------|------|-------------|
| `pageCount` | `number` | Items per page (default: 25) |
| `pageIndex` | `number` | Page number, 0-indexed (default: 0) |
| `pageIndex` | `number` | Page number, 1-indexed — first page is 1 (default: 1) |
| `searchPeriod` | `object` | Date range filter |
| `searchPeriod.startDate` | `string` | Start date: 'YYYY-MM-DD' |
| `searchPeriod.endDate` | `string` | End date: 'YYYY-MM-DD' |
Expand All @@ -457,7 +457,7 @@ console.log(`Found ${invoices.length} invoices`);
// Example 2: Pagination
const page2 = await nfe.serviceInvoices.list('company-id', {
pageCount: 50, // 50 per page
pageIndex: 1, // Second page (0-indexed)
pageIndex: 2, // Second page (1-indexed)
});

// Example 3: Date filtering
Expand All @@ -470,7 +470,7 @@ const lastMonth = await nfe.serviceInvoices.list('company-id', {
});

// Example 4: Process all invoices
let pageIndex = 0;
let pageIndex = 1; // pagination is 1-based
let allInvoices = [];

while (true) {
Expand Down Expand Up @@ -1083,15 +1083,34 @@ const company = await nfe.companies.create({

##### `list(options?: PaginationOptions): Promise<ListResponse<Company>>`

List companies with pagination.
> ⚠️ **Deprecated** — a API v1 de companies está sendo descontinuada. Prefira
> `listV2()` (cursor, v2) para listagem paginada; `listAll()`/`listIterator()`
> para varredura completa. O método continua funcionando durante a convivência.

List companies with pagination (1-based; `pageCount` aceito: 2–50, default 10).

```typescript
const companies = await nfe.companies.list({
pageCount: 20,
pageIndex: 0
pageIndex: 1 // first page (pagination is 1-based)
});
```

##### `listV2(options?: CompanyV2ListOptions): Promise<CompanyV2ListResponse>`

Lista empresas pela API v2 (`api.nfse.io/v2/companies`, cursor-based). `limit`
1–50 (default 10); use o `id` do último item como `startingAfter` para a
próxima página; `hasMore` indica se há mais. Os itens seguem a projeção v2
(`CompanyResourceItem`) — shape diferente do `Company` v1.

```typescript
let page = await nfe.companies.listV2({ limit: 50 });
while (page.hasMore) {
const last = page.data[page.data.length - 1];
page = await nfe.companies.listV2({ limit: 50, startingAfter: last.id });
}
```

##### `listAll(): Promise<Company[]>`

Get all companies (auto-pagination).
Expand Down Expand Up @@ -2765,10 +2784,16 @@ interface PollOptions {
}

interface ListResponse<T> {
items: T[];
totalCount: number;
pageIndex: number;
data: T[];
totalCount?: number;
page?: PageInfo;
}

interface PageInfo {
pageIndex: number; // 1-based — first page is 1
pageCount: number;
hasNext?: boolean;
hasPrevious?: boolean;
}
```

Expand Down
9 changes: 6 additions & 3 deletions openapi/spec/nf-servico-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@ paths:
parameters:
- name: pageCount
in: query
description: Items por página
description: Items por página (a API aceita 2–50 e retorna 10 quando omitido; 1 é rejeitado apesar da mensagem "between 1 and 50")
required: false
schema:
type: integer
format: int32
maximum: 50
- name: pageIndex
in: query
description: Número da página
description: Número da página (1-based — a primeira página é 1)
required: false
schema:
type: integer
format: int32
minimum: 1
responses:
"200":
description: Consulta realizada com sucesso
Expand Down Expand Up @@ -2590,11 +2592,12 @@ paths:
format: int32
- name: pageIndex
in: query
description: Número da página
description: Número da página (1-based — a primeira página é 1)
required: false
schema:
type: integer
format: int32
minimum: 1
- name: issuedBegin
in: query
description: Data de competência início
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nfe-io",
"version": "5.1.0",
"version": "5.2.0",
"description": "Official NFE.io SDK for Node.js - TypeScript native with zero runtime dependencies",
"keywords": [
"nfe",
Expand Down Expand Up @@ -28,7 +28,7 @@
"url": "git+https://github.com/nfe/client-nodejs.git"
},
"bugs": "https://github.com/nfe/client-nodejs/issues",
"homepage": "https://nfe.io",
"homepage": "https://nfe.io/docs/desenvolvedores/bibliotecas/node/",
"engines": {
"node": ">=22.0.0"
},
Expand Down
17 changes: 12 additions & 5 deletions skills/nfeio-node-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ Most resources are scoped to a company. The pattern is always `resource.method(c
```typescript
// List service invoices for a company
const invoices = await nfe.serviceInvoices.list('company-uuid', {
pageIndex: 0,
pageIndex: 1, // pagination is 1-based (the API rejects 0)
pageCount: 50,
});

Expand Down Expand Up @@ -202,24 +202,31 @@ All error objects have: `message`, `type`, `code`/`status`/`statusCode`, `detail

The SDK uses **two different pagination styles**, and the response shape varies by resource:

**Offset-based** (service invoices, companies, people, webhooks):
**Offset-based** (service invoices, companies, people, webhooks) — **1-based**: the first page is `pageIndex: 1`; the API rejects `pageIndex: 0` with a validation error (confirmed live for companies and service invoices):
```typescript
// Service invoices return { serviceInvoices, totalResults, totalPages, page }
const page = await nfe.serviceInvoices.list(companyId, {
pageIndex: 0, // 0-based page number
pageIndex: 1, // 1-based page number (first page = 1)
pageCount: 50, // Items per page
});
page.serviceInvoices; // ServiceInvoiceData[]
page.totalResults; // number
page.totalPages; // number

// Companies, people and webhooks return the generic ListResponse<T>
const companies = await nfe.companies.list({ pageIndex: 0, pageCount: 50 });
const companies = await nfe.companies.list({ pageIndex: 1, pageCount: 50 });
companies.data; // Company[]
companies.totalCount; // number | undefined
companies.page; // { pageIndex, pageCount }
companies.page; // { pageIndex, pageCount } — pageIndex is 1-based as of v5.2.0
```

> ⚠️ Version note: in `nfe-io` ≤ 5.1.x, `companies.list()` subtracted 1 from the response
> (`page.pageIndex` came back 0-based) and `listAll()`/`listIterator()` started at page 0 —
> which the API rejects, so auto-pagination (and `findByTaxNumber`/`findByName`/
> `getCompaniesWithCertificates`/`getCompaniesWithExpiringCertificates`) failed on the first
> call. Fixed in v5.2.0: 1-based on both request and response. The request was ALWAYS 1-based
> on the wire — never send `pageIndex: 0` regardless of SDK version.

**Cursor-based** (product invoices, state taxes):
```typescript
// Returns { productInvoices, hasMore }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ Access via `nfe.taxCodes`. Global scope. Reference data for tax calculation inpu
| `listIssuerTaxProfiles(options?)` | Issuer tax profile codes |
| `listRecipientTaxProfiles(options?)` | Recipient tax profile codes |

All return `TaxCodePaginatedResponse` with `{ data: TaxCode[], totalCount, page }`.
All return `TaxCodePaginatedResponse` with `{ items?: TaxCode[], currentPage?, totalPages?, totalCount? }` (live-verified 2026-07-13 — note it is `items`/`currentPage`, NOT the `data`/`page` shape used by `ListResponse<T>` elsewhere in the SDK). Pagination is 1-based (`pageIndex`, default 1).

```typescript
interface TaxCode {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Throws `PollingTimeoutError` if timeout exceeded. Throws `InvoiceProcessingError

```typescript
interface ListServiceInvoicesOptions {
pageIndex?: number; // 0-based page (default: 0)
pageIndex?: number; // 1-based page — first page is 1; the API rejects 0
pageCount?: number; // Items per page (default: 50)
issuedBegin?: string; // Filter by issue date start (yyyy-MM-dd)
issuedEnd?: string; // Filter by issue date end
Expand Down
Loading
Loading