From e82e16aa237bf0ea43438c8f220c8ace9a081d62 Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Tue, 14 Jul 2026 00:35:17 -0300 Subject: [PATCH 1/4] chore(test): restringe a coleta do vitest a tests/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O glob '**/*.test.ts' atravessava os symlinks client-php/client-ruby (que apontam de volta para este repo), coletando a mesma suíte 5x (210 arquivos/ 3710 testes aparentes vs 42/698 reais) e causando flakes por corrida no .test-temp compartilhado do generation.test.ts. --- vitest.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index f2e9359..e186a7f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,10 @@ export default defineConfig({ test: { globals: true, environment: 'node', - include: ['**/*.test.ts', '**/*.spec.ts'], + // Restrito a tests/: o glob '**/*' atravessava os symlinks client-php/ + // client-ruby (que apontam de volta para este repo), coletando os mesmos + // testes 3-4x e corrompendo o .test-temp compartilhado (flake). + include: ['tests/**/*.test.ts', 'tests/**/*.spec.ts'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], From 100327eae986063b7b5940835c83f9dfc00f3984 Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Tue, 14 Jul 2026 00:36:25 -0300 Subject: [PATCH 2/4] =?UTF-8?q?fix(companies):=20pagina=C3=A7=C3=A3o=201-b?= =?UTF-8?q?ased,=20teto=20de=20pageCount=20e=20sem=C3=A2ntica=20PUT=20docu?= =?UTF-8?q?mentada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contratos provados por sonda ao vivo (2026-07-13/14, duas contas): - listAll()/listIterator() falhavam na 1ª chamada por dois motivos independentes: iniciavam em pageIndex=0 (a API rejeita: 1-based) e pediam pageCount=100 (a API aceita 2-50; rejeita 1 apesar da mensagem 'between 1 and 50'). Corrigidos (início em 1, AUTO_PAGINATION_PAGE_SIZE=50) — conserta transitivamente findByTaxNumber, findByName, getCompaniesWithCertificates e getCompaniesWithExpiringCertificates. Provado: 352 empresas / 8 páginas. - list() deixou de subtrair 1 da resposta: convenção 1-based nos dois lados (nota de migração no CHANGELOG). - JSDoc de update() era enganoso: dizia 'only fields to update', mas a API faz PUT (substituição total) — risco de zerar campos. Documentado com exemplo read-modify-write; create() documenta os obrigatórios reais. - Specs: minimum:1 no pageIndex e maximum:50 no pageCount de /v1/companies. - Testes: mock espelha as validações reais da API; alignment test pina os corpos de escrita (Create/Update iguais = evidência PUT, sem email). - Docs/skills: exemplos 0-based corrigidos em API.md, README e na skill nfeio-node-sdk (que também afirmava shape errado para taxCodes). --- README.md | 28 +++++--- docs/API.md | 22 +++--- openapi/spec/nf-servico-v1.yaml | 9 ++- skills/nfeio-node-sdk/SKILL.md | 17 +++-- .../references/product-invoices-and-taxes.md | 2 +- .../service-invoices-and-polling.md | 2 +- src/core/resources/companies.ts | 71 ++++++++++++++----- src/core/resources/service-invoices.ts | 2 +- src/core/types.ts | 4 +- src/generated/calculo-impostos-v1.ts | 2 +- src/generated/consulta-cte-v2.ts | 2 +- src/generated/consulta-nfe-distribuicao-v1.ts | 2 +- src/generated/contribuintes-v2.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 | 8 +-- src/generated/nfeio.ts | 2 +- src/generated/product-invoice-rtc-v1.ts | 2 +- src/generated/service-invoice-rtc-v1.ts | 2 +- tests/types/company-write-alignment.test-d.ts | 62 ++++++++++++++++ tests/unit/companies.test.ts | 71 +++++++++++++++++++ 22 files changed, 259 insertions(+), 59 deletions(-) create mode 100644 tests/types/company-write-alignment.test-d.ts diff --git a/README.md b/README.md index 584a8a8..0ade324 100644 --- a/README.md +++ b/README.md @@ -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', @@ -259,22 +259,34 @@ 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 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 diff --git a/docs/API.md b/docs/API.md index 274c5cf..d9e8f94 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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' | @@ -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 @@ -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) { @@ -1083,12 +1083,12 @@ const company = await nfe.companies.create({ ##### `list(options?: PaginationOptions): Promise>` -List companies with pagination. +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) }); ``` @@ -2765,10 +2765,16 @@ interface PollOptions { } interface ListResponse { - 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; } ``` diff --git a/openapi/spec/nf-servico-v1.yaml b/openapi/spec/nf-servico-v1.yaml index ed59a13..9603369 100644 --- a/openapi/spec/nf-servico-v1.yaml +++ b/openapi/spec/nf-servico-v1.yaml @@ -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 @@ -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 diff --git a/skills/nfeio-node-sdk/SKILL.md b/skills/nfeio-node-sdk/SKILL.md index 166b89c..1932dd0 100644 --- a/skills/nfeio-node-sdk/SKILL.md +++ b/skills/nfeio-node-sdk/SKILL.md @@ -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, }); @@ -202,11 +202,11 @@ 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[] @@ -214,12 +214,19 @@ page.totalResults; // number page.totalPages; // number // Companies, people and webhooks return the generic ListResponse -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 } diff --git a/skills/nfeio-node-sdk/references/product-invoices-and-taxes.md b/skills/nfeio-node-sdk/references/product-invoices-and-taxes.md index f8d8970..f5fa81b 100644 --- a/skills/nfeio-node-sdk/references/product-invoices-and-taxes.md +++ b/skills/nfeio-node-sdk/references/product-invoices-and-taxes.md @@ -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` elsewhere in the SDK). Pagination is 1-based (`pageIndex`, default 1). ```typescript interface TaxCode { diff --git a/skills/nfeio-node-sdk/references/service-invoices-and-polling.md b/skills/nfeio-node-sdk/references/service-invoices-and-polling.md index 785036d..ceb30f1 100644 --- a/skills/nfeio-node-sdk/references/service-invoices-and-polling.md +++ b/skills/nfeio-node-sdk/references/service-invoices-and-polling.md @@ -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 diff --git a/src/core/resources/companies.ts b/src/core/resources/companies.ts index c2a5a7f..9cac834 100644 --- a/src/core/resources/companies.ts +++ b/src/core/resources/companies.ts @@ -13,6 +13,10 @@ import type { HttpClient } from '../http/client.js'; import { ValidationError, NotFoundError } from '../errors/index.js'; import { CertificateValidator } from '../utils/certificate-validator.js'; +// Page size for listAll/listIterator: the API caps GET /companies at +// pageCount 50 (values above 50 — and also 1 — are rejected with a 400). +const AUTO_PAGINATION_PAGE_SIZE = 50; + // ============================================================================ // Validation Helpers // ============================================================================ @@ -149,6 +153,13 @@ export class CompaniesResource { /** * Create a new company * + * The API requires `name`, `federalTaxNumber`, `taxRegime` and `address` + * (with `state`, `city { code, name }`, `district`, `street`, `number`, + * `postalCode`, `country`) — a payload without them compiles against the + * loose `Company`-based signature but fails with a 400. Note that `email` + * is NOT part of the create body. For the strict wire shape, see + * {@link CreateCompanyResourceItem} (exported from the package root). + * * @param data - Company data (excluding id, createdOn, modifiedOn) * @returns The created company with generated id * @throws {ValidationError} If company data is invalid @@ -159,8 +170,17 @@ export class CompaniesResource { * ```typescript * const company = await nfe.companies.create({ * name: 'Acme Corp', - * federalTaxNumber: 12345678901234, - * email: 'contact@acme.com' + * 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', + * }, * }); * ``` */ @@ -178,13 +198,20 @@ export class CompaniesResource { /** * List companies * + * Pagination is 1-based (API contract): the first page is `pageIndex: 1`. + * The API rejects `pageIndex: 0` with a validation error. + * + * `pageCount` accepted by the API: 2-50 (when omitted, the API returns 10 + * items). Values outside that range — including 1, despite the API's + * "between 1 and 50" error message — are rejected with a 400. + * * @param options - Pagination options (pageCount, pageIndex) * @returns List response with companies and pagination info * * @example * ```typescript - * const page1 = await nfe.companies.list({ pageCount: 20, pageIndex: 0 }); - * const page2 = await nfe.companies.list({ pageCount: 20, pageIndex: 1 }); + * const page1 = await nfe.companies.list({ pageCount: 20, pageIndex: 1 }); + * const page2 = await nfe.companies.list({ pageCount: 20, pageIndex: 2 }); * ``` */ async list(options: PaginationOptions = {}): Promise> { @@ -192,12 +219,12 @@ export class CompaniesResource { const response = await this.http.get<{ companies: Company[]; page: number }>(path, options); // API returns: { companies: [...], page: number } - // Transform to our standard ListResponse format + // Transform to our standard ListResponse format (pageIndex stays 1-based, as on the wire) return { data: response.data.companies, page: { - pageIndex: response.data.page - 1, // API uses 1-based, we use 0-based - pageCount: options.pageCount || 100, + pageIndex: response.data.page, + pageCount: options.pageCount ?? 10, // the API returns 10 items when pageCount is omitted } }; } @@ -218,16 +245,16 @@ export class CompaniesResource { */ async listAll(): Promise { const companies: Company[] = []; - let pageIndex = 0; + let pageIndex = 1; // pagination is 1-based; the API rejects pageIndex 0 let hasMore = true; while (hasMore) { - const page = await this.list({ pageCount: 100, pageIndex }); + const page = await this.list({ pageCount: AUTO_PAGINATION_PAGE_SIZE, pageIndex }); const pageData = Array.isArray(page) ? page : (page.data || []); companies.push(...pageData); // Check if there are more pages - hasMore = pageData.length === 100; + hasMore = pageData.length === AUTO_PAGINATION_PAGE_SIZE; pageIndex++; } @@ -250,18 +277,18 @@ export class CompaniesResource { * ``` */ async *listIterator(): AsyncIterableIterator { - let pageIndex = 0; + let pageIndex = 1; // pagination is 1-based; the API rejects pageIndex 0 let hasMore = true; while (hasMore) { - const page = await this.list({ pageCount: 100, pageIndex }); + const page = await this.list({ pageCount: AUTO_PAGINATION_PAGE_SIZE, pageIndex }); const pageData = Array.isArray(page) ? page : (page.data || []); for (const company of pageData) { yield company; } - hasMore = pageData.length === 100; + hasMore = pageData.length === AUTO_PAGINATION_PAGE_SIZE; pageIndex++; } } @@ -291,17 +318,29 @@ export class CompaniesResource { /** * Update a company * + * **This is a PUT (full replacement), NOT a partial update.** The API + * requires the complete object (`name`, `federalTaxNumber`, `taxRegime`, + * `address`, ...) on every call; omitted fields are reset/replaced, not + * kept. Sending only the fields you want to change either fails with a + * 400 or silently wipes the rest. Always read-modify-write. + * + * For the strict wire shape, see {@link UpdateCompanyResourceItem} + * (exported from the package root). The loose `Partial` + * signature is kept for backwards compatibility only. + * * @param companyId - Company ID to update - * @param data - Partial company data (only fields to update) + * @param data - The COMPLETE company data (full replacement) * @returns The updated company * @throws {ValidationError} If update data is invalid * @throws {NotFoundError} If company doesn't exist * * @example * ```typescript + * // Read-modify-write: fetch the current object, change it, send it whole + * const current = await nfe.companies.retrieve('company-123'); * const updated = await nfe.companies.update('company-123', { - * name: 'New Name', - * email: 'new@example.com' + * ...current, + * tradeName: 'Novo Nome Fantasia', * }); * ``` */ diff --git a/src/core/resources/service-invoices.ts b/src/core/resources/service-invoices.ts index 692d486..b43ff26 100644 --- a/src/core/resources/service-invoices.ts +++ b/src/core/resources/service-invoices.ts @@ -140,7 +140,7 @@ export class ServiceInvoicesResource { * ```typescript * // List recent invoices * const result = await nfe.serviceInvoices.list(companyId, { - * pageIndex: 0, + * pageIndex: 1, * pageCount: 20, * issuedBegin: '2026-01-01', * issuedEnd: '2026-01-31' diff --git a/src/core/types.ts b/src/core/types.ts index 5405dd6..c9ca729 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -356,7 +356,7 @@ export interface ListResponse { } export interface PageInfo { - /** Current page index */ + /** Current page index (1-based — the first page is 1) */ pageIndex: number; /** Items per page */ pageCount: number; @@ -367,7 +367,7 @@ export interface PageInfo { } export interface PaginationOptions extends Record { - /** Page index (0-based) */ + /** Page index (1-based — the first page is 1; the API rejects 0) */ pageIndex?: number; /** Items per page */ pageCount?: number; diff --git a/src/generated/calculo-impostos-v1.ts b/src/generated/calculo-impostos-v1.ts index 59a9d07..f35b41c 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-06-27T15:46:53.795Z + * Last generated: 2026-07-14T03:15:10.362Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-cte-v2.ts b/src/generated/consulta-cte-v2.ts index 2e1bd94..1ef2934 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-06-27T15:46:53.815Z + * Last generated: 2026-07-14T03:15:10.382Z * Generator: openapi-typescript */ diff --git a/src/generated/consulta-nfe-distribuicao-v1.ts b/src/generated/consulta-nfe-distribuicao-v1.ts index cb0d1cf..7fe6106 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-06-27T15:46:53.844Z + * Last generated: 2026-07-14T03:15:10.412Z * Generator: openapi-typescript */ diff --git a/src/generated/contribuintes-v2.ts b/src/generated/contribuintes-v2.ts index a54ded2..aedc60b 100644 --- a/src/generated/contribuintes-v2.ts +++ b/src/generated/contribuintes-v2.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-06-27T15:46:53.895Z + * Last generated: 2026-07-14T03:15:10.465Z * Generator: openapi-typescript */ diff --git a/src/generated/index.ts b/src/generated/index.ts index 55da5fe..37560a9 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-06-27T15:46:54.263Z + * Last updated: 2026-07-14T03:15:10.827Z */ // ============================================================================ diff --git a/src/generated/nf-consumidor-v2.ts b/src/generated/nf-consumidor-v2.ts index 575a7a7..0648896 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-06-27T15:46:54.008Z + * Last generated: 2026-07-14T03:15:10.579Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-produto-v2.ts b/src/generated/nf-produto-v2.ts index 24df3b5..6fd5a99 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-06-27T15:46:54.094Z + * Last generated: 2026-07-14T03:15:10.663Z * Generator: openapi-typescript */ diff --git a/src/generated/nf-servico-v1.ts b/src/generated/nf-servico-v1.ts index a99a507..5610eb0 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-06-27T15:46:54.166Z + * Last generated: 2026-07-14T03:15:10.733Z * Generator: openapi-typescript */ @@ -1330,9 +1330,9 @@ export interface operations { readonly Companies_Get: { readonly parameters: { readonly 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") */ readonly pageCount?: number; - /** @description Número da página */ + /** @description Número da página (1-based — a primeira página é 1) */ readonly pageIndex?: number; }; readonly header?: never; @@ -3370,7 +3370,7 @@ export interface operations { readonly query?: { /** @description Items por página */ readonly pageCount?: number; - /** @description Número da página */ + /** @description Número da página (1-based — a primeira página é 1) */ readonly pageIndex?: number; /** @description Data de competência início */ readonly issuedBegin?: string; diff --git a/src/generated/nfeio.ts b/src/generated/nfeio.ts index fc042d8..22176e7 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-06-27T15:46:54.177Z + * Last generated: 2026-07-14T03:15:10.744Z * Generator: openapi-typescript */ diff --git a/src/generated/product-invoice-rtc-v1.ts b/src/generated/product-invoice-rtc-v1.ts index 7b88e2f..4e05bb1 100644 --- a/src/generated/product-invoice-rtc-v1.ts +++ b/src/generated/product-invoice-rtc-v1.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-06-27T15:46:54.234Z + * Last generated: 2026-07-14T03:15:10.802Z * Generator: openapi-typescript */ diff --git a/src/generated/service-invoice-rtc-v1.ts b/src/generated/service-invoice-rtc-v1.ts index f917094..6844187 100644 --- a/src/generated/service-invoice-rtc-v1.ts +++ b/src/generated/service-invoice-rtc-v1.ts @@ -4,7 +4,7 @@ * Do not edit this file directly. * * To regenerate: npm run generate - * Last generated: 2026-06-27T15:46:54.261Z + * Last generated: 2026-07-14T03:15:10.826Z * Generator: openapi-typescript */ diff --git a/tests/types/company-write-alignment.test-d.ts b/tests/types/company-write-alignment.test-d.ts new file mode 100644 index 0000000..c0a472d --- /dev/null +++ b/tests/types/company-write-alignment.test-d.ts @@ -0,0 +1,62 @@ +/** + * Alignment guard: os corpos de escrita de companies amarrados aos schemas + * gerados da contribuintes-v2 (`CreateCompanyResourceItem` / + * `UpdateCompanyResourceItem`). + * + * As assinaturas públicas de `create()`/`update()` seguem frouxas + * (`Omit` / `Partial`, compat legada — tightening fica para + * a próxima major ou para a migração de CRUD v2). Este arquivo PINA o contrato + * real do fio no nível de tipo: se um sync de spec mudar os obrigatórios do + * corpo de create/update, o `npm run test:types` quebra em vez de driftar. + * + * Fatos pinados (nota Obsidian "companies create-update com tipagem frouxa", + * 2026-07-13): + * - create e update exigem o MESMO conjunto: name, federalTaxNumber, + * taxRegime, address — update é PUT (substituição total), não PATCH; + * - `email` NÃO existe no corpo de escrita (apesar de o tipo `Company` + * legado exigi-lo). + */ + +import { describe, it, expectTypeOf } from 'vitest'; +import type { + CreateCompanyResourceItem, + UpdateCompanyResourceItem, +} from '../../src/index.js'; + +// Chaves obrigatórias de um objeto (as que não aceitam undefined por omissão) +type RequiredKeys = { + [K in keyof T]-?: object extends Pick ? never : K; +}[keyof T]; + +describe('Corpos de escrita de companies ↔ schemas gerados (contribuintes-v2)', () => { + it('create exige exatamente name, federalTaxNumber, taxRegime e address', () => { + expectTypeOf>().toEqualTypeOf< + 'name' | 'federalTaxNumber' | 'taxRegime' | 'address' + >(); + }); + + it('update (PUT, substituição total) exige o MESMO conjunto do create', () => { + expectTypeOf>().toEqualTypeOf< + RequiredKeys + >(); + }); + + it('email NÃO faz parte do corpo de escrita (create nem update)', () => { + expectTypeOf<'email'>().not.toExtend(); + expectTypeOf<'email'>().not.toExtend(); + }); + + it('update aceita id opcional além do conjunto do create (única diferença)', () => { + expectTypeOf< + Exclude + >().toEqualTypeOf<'id'>(); + }); + + it('tipos de contrato dos obrigatórios', () => { + const c = {} as CreateCompanyResourceItem; + expectTypeOf(c.name).toEqualTypeOf(); + expectTypeOf(c.federalTaxNumber).toEqualTypeOf(); + expectTypeOf(c.address).not.toBeNever(); + expectTypeOf(c.taxRegime).not.toBeNever(); + }); +}); diff --git a/tests/unit/companies.test.ts b/tests/unit/companies.test.ts index 0df2bab..fe229b4 100644 --- a/tests/unit/companies.test.ts +++ b/tests/unit/companies.test.ts @@ -3,6 +3,7 @@ import { CompaniesResource } from '../../src/core/resources/companies'; import type { HttpClient } from '../../src/core/http/client'; import type { HttpResponse, ListResponse, Company } from '../../src/core/types'; import { createMockCompany, TEST_COMPANY_ID } from '../setup'; +import { ValidationError } from '../../src/core/errors/index.js'; import { CertificateValidator } from '../../src/core/utils/certificate-validator'; // Mock CertificateValidator to avoid certificate format validation issues in tests @@ -63,6 +64,76 @@ describe('CompaniesResource', () => { }); }); + describe('pagination (1-based API contract)', () => { + // Mirrors the real API (probed live 2026-07-13): GET /companies rejects + // pageIndex < 1, and accepts pageCount only in 2-50 (1 is rejected too, + // despite the API's "between 1 and 50" error message) + const mirrorApiGet = (pages: Company[][]) => + vi + .fn() + .mockImplementation( + (_path: string, options: { pageIndex?: number; pageCount?: number } = {}) => { + const pageIndex = options.pageIndex ?? 1; + if (pageIndex < 1) { + return Promise.reject(new ValidationError('pageIndex must be greater or equal to 1')); + } + if (options.pageCount !== undefined && (options.pageCount < 2 || options.pageCount > 50)) { + return Promise.reject(new ValidationError('pageCount must be between 1 and 50')); + } + return Promise.resolve({ + data: { companies: pages[pageIndex - 1] ?? [], page: pageIndex }, + status: 200, + headers: {}, + }); + } + ); + + it('list returns page.pageIndex exactly as the API sent it (no 0-based normalization)', async () => { + mockHttpClient.get = mirrorApiGet([[createMockCompany()]]); + + const result = await companies.list({ pageIndex: 1 }); + + expect(result.page?.pageIndex).toBe(1); + }); + + it('list({ pageIndex: 0 }) is rejected by the API contract', async () => { + mockHttpClient.get = mirrorApiGet([[createMockCompany()]]); + + await expect(companies.list({ pageIndex: 0 })).rejects.toThrow( + 'pageIndex must be greater or equal to 1' + ); + }); + + it('listAll starts at pageIndex 1, respects the pageCount cap, and paginates 1 -> 2', async () => { + const fullPage = Array.from({ length: 50 }, (_, i) => + createMockCompany({ id: `company-${i}` }) + ); + const lastPage = [createMockCompany({ id: 'company-last' })]; + mockHttpClient.get = mirrorApiGet([fullPage, lastPage]); + + const result = await companies.listAll(); + + expect(result).toHaveLength(51); + expect(vi.mocked(mockHttpClient.get).mock.calls[0][1]).toMatchObject({ + pageIndex: 1, + pageCount: 50, + }); + expect(vi.mocked(mockHttpClient.get).mock.calls[1][1]).toMatchObject({ pageIndex: 2 }); + }); + + it('listIterator starts at pageIndex 1', async () => { + mockHttpClient.get = mirrorApiGet([[createMockCompany({ id: 'company-1' })]]); + + const seen: Company[] = []; + for await (const company of companies.listIterator()) { + seen.push(company); + } + + expect(seen).toHaveLength(1); + expect(vi.mocked(mockHttpClient.get).mock.calls[0][1]).toMatchObject({ pageIndex: 1 }); + }); + }); + describe('retrieve', () => { it('should retrieve a specific company', async () => { const mockCompany = createMockCompany(); From 3051dfca7c6b5d8c62c5d23ee06f47339d6a0ca9 Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Tue, 14 Jul 2026 00:36:43 -0300 Subject: [PATCH 3/4] =?UTF-8?q?feat(companies):=20listV2=20(API=20v2,=20cu?= =?UTF-8?q?rsor-based)=20e=20depreca=C3=A7=C3=A3o=20do=20list()=20v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A API v1 de companies (api.nfe.io/v1/companies) está sendo descontinuada; o substituto é a contribuintes-v2 (api.nfse.io/v2/companies), cursor-based. - listV2({ limit, startingAfter, endingBefore }) -> { data, hasMore }, via o client v2 já injetado. Contrato provado ao vivo (2026-07-14): limit 1-50 (default 10), >50 rejeitado; limit=0 devolveria página vazia silenciosa e é rejeitado client-side. Itens seguem a projeção v2 (CompanyResourceItem), shape distinto do Company v1. Tipos: CompanyV2ListOptions/Response. - list() v1 marcado @deprecated apontando para listV2/listAll/listIterator; segue funcional durante a convivência. - listAll()/listIterator() permanecem no transporte v1 nesta release: o gate de paridade reprovou — enumeração completa via v2 quebra com 500 determinístico num registro específico (bug reportado ao backend) e as projeções v1/v2 divergem (inclusive municipalTaxNumber com valores diferentes). Pendências rastreadas em #41. Refs #41 --- README.md | 4 ++ docs/API.md | 19 +++++++++ src/core/resources/companies.ts | 65 ++++++++++++++++++++++++++++++- src/core/types.ts | 28 +++++++++++++ src/index.ts | 2 + tests/unit/companies.test.ts | 69 +++++++++++++++++++++++++++++++++ 6 files changed, 186 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ade324..6b999f7 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,10 @@ const empresa = await nfe.companies.create({ }, }); +// 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: }) + // Listar todas as empresas (varredura completa com paginação automática) const empresas = await nfe.companies.listAll(); diff --git a/docs/API.md b/docs/API.md index d9e8f94..c3393c8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1083,6 +1083,10 @@ const company = await nfe.companies.create({ ##### `list(options?: PaginationOptions): Promise>` +> ⚠️ **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 @@ -1092,6 +1096,21 @@ const companies = await nfe.companies.list({ }); ``` +##### `listV2(options?: CompanyV2ListOptions): Promise` + +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` Get all companies (auto-pagination). diff --git a/src/core/resources/companies.ts b/src/core/resources/companies.ts index 9cac834..8439845 100644 --- a/src/core/resources/companies.ts +++ b/src/core/resources/companies.ts @@ -6,6 +6,9 @@ import type { Company, + CompanyResourceItem, + CompanyV2ListOptions, + CompanyV2ListResponse, ListResponse, PaginationOptions } from '../types.js'; @@ -196,7 +199,12 @@ export class CompaniesResource { } /** - * List companies + * List companies (v1 API — offset pagination) + * + * @deprecated The v1 companies API (`api.nfe.io/v1/companies`) is being + * discontinued. Prefer {@link listV2} (cursor-based, `api.nfse.io/v2`) for + * page-by-page listing, or {@link listAll}/{@link listIterator} for full + * sweeps. This method keeps working during the coexistence window. * * Pagination is 1-based (API contract): the first page is `pageIndex: 1`. * The API rejects `pageIndex: 0` with a validation error. @@ -229,6 +237,61 @@ export class CompaniesResource { }; } + /** + * List companies via the v2 cursor API (`GET api.nfse.io/v2/companies`) + * + * This is the successor of {@link list} (the v1 companies API is being + * discontinued). Cursor-based: pass the last item's `id` as + * `startingAfter` to fetch the next page; `hasMore` tells whether more + * pages exist. Results are ordered by name, then id. + * + * `limit` accepted by the API: 1-50 (default 10). Values above 50 are + * rejected; `limit: 0` is rejected client-side (the API would silently + * return an empty page). Items follow the **v2 projection** + * ({@link CompanyResourceItem}) — a different shape from the v1 + * {@link Company} (no NFS-e config fields; adds `stateTaxes`, + * `municipalTaxes`, `type`, `version`). + * + * Known API issue (reported 2026-07-14): on some accounts, specific + * records make the server answer 500 for any page window containing + * them, which breaks full sweeps — the reason {@link listAll}/ + * {@link listIterator} still run on v1 in this release. + * + * @param options - Cursor pagination options (limit, startingAfter, endingBefore) + * @returns Page of companies (v2 projection) plus `hasMore` + * @throws {ValidationError} If `limit` is outside 1-50 + * + * @example + * ```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 }); + * } + * ``` + */ + async listV2(options: CompanyV2ListOptions = {}): Promise { + if (options.limit !== undefined && (options.limit < 1 || options.limit > 50)) { + throw new ValidationError('limit must be between 1 and 50'); + } + + const params: Record = {}; + if (options.limit !== undefined) params.limit = options.limit; + if (options.startingAfter) params.startingAfter = options.startingAfter; + if (options.endingBefore) params.endingBefore = options.endingBefore; + + // Wire response: { hasMore, companies } (the spec omits hasMore; the live API sends it) + const response = await this.v2Http.get<{ + hasMore?: boolean; + companies?: CompanyResourceItem[] | null; + }>('/v2/companies', params); + + return { + data: (response.data.companies ?? []) as CompanyResourceItem[], + hasMore: response.data.hasMore ?? false, + }; + } + /** * List all companies with automatic pagination * diff --git a/src/core/types.ts b/src/core/types.ts index c9ca729..806ced8 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -555,6 +555,34 @@ export type CreateCompanyResourceItem = export type UpdateCompanyResourceItem = ContribuintesComponents['schemas']['DFeTech.TaxPayers.Resources.UpdateCompanyResourceItem']; +/** + * Options for the v2 cursor-based company listing + * (`GET api.nfse.io/v2/companies`, contribuintes-v2). + */ +export interface CompanyV2ListOptions { + /** Cursor: start after this company ID */ + startingAfter?: string; + /** Cursor: end before this company ID */ + endingBefore?: string; + /** Number of results per page — the API accepts 1-50 (default: 10) */ + limit?: number; +} + +/** + * Response of the v2 cursor-based company listing. + * + * Items follow the v2 projection ({@link CompanyResourceItem}) — a different + * shape from the v1 {@link Company}: no NFS-e config fields (`rpsNumber`, + * `issRate`, `environment`, `fiscalStatus`, `certificate`, ...), and with + * v2-only fields (`stateTaxes`, `municipalTaxes`, `type`, `version`). + */ +export interface CompanyV2ListResponse { + /** Companies in this page (v2 projection) */ + data: CompanyResourceItem[]; + /** Whether more pages exist after this one */ + hasMore: boolean; +} + /** Digital certificate metadata (real, spec-backed). */ export type CertificateMetadataResource = ContribuintesComponents['schemas']['DFeTech.TaxPayers.Resources.CertificateMetadataResource']; diff --git a/src/index.ts b/src/index.ts index d5498a4..1f53301 100644 --- a/src/index.ts +++ b/src/index.ts @@ -328,6 +328,8 @@ export type { CompanyResourceV1, CreateCompanyResourceItem, UpdateCompanyResourceItem, + CompanyV2ListOptions, + CompanyV2ListResponse, CertificateMetadataResource, CompanyAddress, MunicipalTax, diff --git a/tests/unit/companies.test.ts b/tests/unit/companies.test.ts index fe229b4..cb9a702 100644 --- a/tests/unit/companies.test.ts +++ b/tests/unit/companies.test.ts @@ -134,6 +134,75 @@ describe('CompaniesResource', () => { }); }); + describe('listV2 (v2 cursor API contract)', () => { + // Mirrors the real v2 API (probed live 2026-07-14): GET /v2/companies + // returns { hasMore, companies }; limit above 50 is rejected with + // "limit must be less than 50"; limit=0 returns 200 with an empty page. + const mirrorV2Get = (pages: Company[][]) => + vi + .fn() + .mockImplementation( + (_path: string, options: { limit?: number; startingAfter?: string } = {}) => { + if (options.limit !== undefined && options.limit > 50) { + return Promise.reject(new ValidationError('limit must be less than 50')); + } + const pageIdx = options.startingAfter + ? pages.findIndex((p) => p.some((c) => c.id === options.startingAfter)) + 1 + : 0; + return Promise.resolve({ + data: { companies: pages[pageIdx] ?? [], hasMore: pageIdx < pages.length - 1 }, + status: 200, + headers: {}, + }); + } + ); + + it('returns { data, hasMore } and hits /v2/companies', async () => { + mockHttpClient.get = mirrorV2Get([[createMockCompany({ id: 'v2-1' })]]); + + const result = await companies.listV2({ limit: 10 }); + + expect(result.data).toHaveLength(1); + expect(result.hasMore).toBe(false); + expect(vi.mocked(mockHttpClient.get).mock.calls[0][0]).toBe('/v2/companies'); + expect(vi.mocked(mockHttpClient.get).mock.calls[0][1]).toMatchObject({ limit: 10 }); + }); + + it('follows the cursor with startingAfter across pages', async () => { + const page1 = [createMockCompany({ id: 'v2-1' }), createMockCompany({ id: 'v2-2' })]; + const page2 = [createMockCompany({ id: 'v2-3' })]; + mockHttpClient.get = mirrorV2Get([page1, page2]); + + const first = await companies.listV2({ limit: 2 }); + expect(first.hasMore).toBe(true); + + const second = await companies.listV2({ limit: 2, startingAfter: 'v2-2' }); + expect(second.data.map((c) => c.id)).toEqual(['v2-3']); + expect(second.hasMore).toBe(false); + }); + + it('rejects limit 0 and 51 client-side before any HTTP call', async () => { + mockHttpClient.get = mirrorV2Get([[]]); + + await expect(companies.listV2({ limit: 0 })).rejects.toThrow('limit must be between 1 and 50'); + await expect(companies.listV2({ limit: 51 })).rejects.toThrow('limit must be between 1 and 50'); + expect(mockHttpClient.get).not.toHaveBeenCalled(); + }); + + it('handles a null companies array defensively', async () => { + mockHttpClient.get = vi.fn().mockResolvedValue({ + data: { companies: null, hasMore: false }, + status: 200, + headers: {}, + }); + + const result = await companies.listV2(); + + expect(result.data).toEqual([]); + expect(result.hasMore).toBe(false); + }); + }); + describe('retrieve', () => { it('should retrieve a specific company', async () => { const mockCompany = createMockCompany(); From 063b360514ecce398ad315e40e1da00a7d8ce9be Mon Sep 17 00:00:00 2001 From: Andre Kutianski Date: Tue, 14 Jul 2026 09:11:16 -0300 Subject: [PATCH 4/4] chore(release): v5.2.0 --- CHANGELOG.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++- package-lock.json | 4 +-- package.json | 4 +-- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fac8b5..011cc15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`/ + `Omit`) 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 @@ -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 diff --git a/package-lock.json b/package-lock.json index 6b9b4ab..d2cfa40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nfe-io", - "version": "5.0.0", + "version": "5.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nfe-io", - "version": "5.0.0", + "version": "5.2.0", "license": "MIT", "devDependencies": { "@types/node": "^22.19.21", diff --git a/package.json b/package.json index 10a171c..8345415 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" },