diff --git a/CHANGELOG.md b/CHANGELOG.md index f055016..47a2f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ e este projeto segue [Versionamento Semântico](https://semver.org/lang/pt-BR/sp ## [Unreleased] +## [3.4.0] — 2026-07-30 + +### Depreciado + +- **`Environment::Sandbox`** (`fix-sandbox-environment-contract`): o case prometia + roteamento/isolamento que **nunca existiu** — não há host sandbox na plataforma + (o Node valida só `production|development`; a spec `client-core` já dizia que o + servidor distingue por credencial). `Config::baseUrlForApi()` nunca consultou o + environment: **todo tráfego sempre foi para produção**, e um integrador que + selecionava `Sandbox` com chave de produção emitia documento fiscal real + achando-se isolado. Agora selecionar `Sandbox` emite `E_USER_DEPRECATED` na + construção do `Config`, explicando o isolamento real (chave de conta de + desenvolvimento + empresa com `environment = Development`). Docblocks de + `Environment`/`Config`, docs (`configuration.md` ganhou a seção "Ambientes na + NFE.io", `getting-started.md`, README) e skill corrigidos. Nenhum comportamento + de rede muda (pinado por teste: URLs idênticas com `Production` e `Sandbox`). + Remoção do case na próxima major. + ### Corrigido - **Domínio de documentos de entrada inteiro** (`fix-inbound-routes`): os 20 métodos diff --git a/README.md b/README.md index f5e08b3..153da3a 100755 --- a/README.md +++ b/README.md @@ -707,7 +707,7 @@ $nfe = new Client(config: $config); |---|---|---| | `apiKey` | obrigatório | Chave principal (emissão, empresas, webhooks). | | `dataApiKey` | `null` | Chave separada para serviços de dados. Quando `null`, faz fallback para `apiKey`. | -| `environment` | `Production` | `Production` ou `Sandbox`. | +| `environment` | `Production` | Metadado sem efeito em roteamento. `Sandbox` está **deprecated** (não há host sandbox — isole por chave + empresa `Development`). | | `timeout` | `60` | Timeout HTTP por requisição (segundos). | | `retry` | `new RetryPolicy()` | Backoff exponencial com jitter simétrico. Use `RetryPolicy::none()` para desabilitar. | | `transport` | `CurlTransport` | Implementação de `Nfe\Http\Transport` (ex.: adaptador PSR-18). | diff --git a/docs/configuration.md b/docs/configuration.md index 63994c1..8c9cef8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -35,7 +35,7 @@ $nfe = new Client( | `apiKey` | `?string` | `null` | Chave principal. Obrigatória quando `config` não é informado. | | `dataApiKey` | `?string` | `null` | Chave dos serviços de dados (CEP/CNPJ/CPF/consultas). Fallback para `apiKey`. | | `config` | `?Nfe\Config` | `null` | Quando informado, **os demais argumentos de conveniência são ignorados**. | -| `environment` | `Nfe\Environment` | `Environment::Production` | `Production` ou `Sandbox`. Reservado para uso futuro (sem efeito sobre os endpoints hoje). | +| `environment` | `Nfe\Environment` | `Environment::Production` | Metadado declarativo — **nunca** altera endpoint ou chave. `Sandbox` está **deprecated** (emite `E_USER_DEPRECATED`); veja [Ambientes na NFE.io](#ambientes-na-nfeio). | | `timeout` | `int` | `60` | Timeout por requisição, em segundos (deve ser positivo). | | `transport` | `?Nfe\Http\Transport` | `null` | Transporte HTTP customizado (adaptador PSR-18, mock). `null` = cURL padrão. | | `userAgentSuffix` | `?string` | `null` | Sufixo anexado ao `User-Agent` do SDK. | @@ -186,17 +186,28 @@ Enquanto isso, a emissão retry-safe se faz com `externalId` — veja Quando a API honrar o header, ele entrará em uma release menor aditiva. ::: -## Sandbox vs. Produção +## Ambientes na NFE.io -:::warning A separação produção vs. teste fica na conta, não no SDK -A escolha entre **produção** e **teste (homologação)** é definida na -configuração da sua conta em [app.nfe.io](https://app.nfe.io) (lado servidor) — -**não** pela chave de API nem pelo SDK. +:::danger `Environment::Sandbox` não isola nada — deprecated +**Não existe host sandbox na plataforma NFE.io.** Selecionar +`Environment::Sandbox` nunca redirecionou tráfego: toda requisição vai para os +hosts de produção, e com uma chave de produção você **emite documento fiscal +real**. Desde a v3.4.0 o case está `@deprecated` e emite `E_USER_DEPRECATED` +na construção do `Config`; será removido na próxima major. ::: -O enum `Nfe\Environment` (`Production` / `Sandbox`) é aceito e validado, mas -hoje **não altera** endpoints, chaves ou comportamento — está reservado para uso -futuro. +O isolamento entre produção e teste na NFE.io acontece em duas camadas — nenhuma +delas é o SDK: + +1. **Escopo da chave**: use a chave de API de uma conta/ambiente de + desenvolvimento para testar. +2. **Ambiente da empresa**: cada empresa cadastrada tem + `environment = Development | Production` (definido em + [app.nfe.io](https://app.nfe.io)); documentos emitidos por uma empresa + `Development` vão para homologação da SEFAZ/prefeitura. + +O enum `Nfe\Environment` permanece como metadado declarativo por +compatibilidade — ele **não altera** endpoints, chaves ou comportamento. :::note Ambiente SEFAZ é outra coisa Os recursos de produto e consumidor (NF-e/NFC-e) aceitam um parâmetro diff --git a/docs/getting-started.md b/docs/getting-started.md index 6efaba8..0ebcb11 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -117,9 +117,12 @@ considerar a nota emitida. Veja ::: :::warning Produção vs. teste -A separação **produção vs. teste (homologação)** é definida na configuração da -sua conta em [app.nfe.io](https://app.nfe.io) — **não** pela chave de API nem -pelo SDK. O argumento `environment:` do cliente está reservado para uso futuro. +A separação **produção vs. teste (homologação)** vem do escopo da sua chave e +do ambiente da empresa (`Development`/`Production`, definido em +[app.nfe.io](https://app.nfe.io)) — **nunca** do SDK. O argumento +`environment:` do cliente é metadado sem efeito, e `Environment::Sandbox` está +**deprecated** (não existe host sandbox; com chave de produção você emitiria +documento real). Veja [Ambientes na NFE.io](./configuration.md#ambientes-na-nfeio). ::: ## Próximos passos diff --git a/skills/nfeio-php-sdk/SKILL.md b/skills/nfeio-php-sdk/SKILL.md index 4d3109b..4de8dcc 100644 --- a/skills/nfeio-php-sdk/SKILL.md +++ b/skills/nfeio-php-sdk/SKILL.md @@ -37,7 +37,7 @@ use Nfe\Environment; $nfe = new Nfe\Client( apiKey: $_ENV['NFE_API_KEY'], // required (unless you pass a Config) dataApiKey: $_ENV['NFE_DATA_API_KEY'] ?? null, // optional, for data services (see below) - environment: Environment::Production, // Environment::Production | Environment::Sandbox + environment: Environment::Production, // metadata only — never routes; Environment::Sandbox is deprecated (no sandbox host exists; isolate via dev-account key + company environment = Development) timeout: 60, // seconds (default 60) userAgentSuffix: 'my-integrator/1.0', // optional ); diff --git a/src/Config.php b/src/Config.php index 04782c5..c724b65 100644 --- a/src/Config.php +++ b/src/Config.php @@ -31,7 +31,9 @@ * (CEP/CNPJ/CPF/NF-e query). Falls back to * $apiKey when null. Mirrors Node SDK * resolveDataApiKey(). - * @param Environment $environment Production or Sandbox routing. + * @param Environment $environment Declarative metadata only — nunca altera host + * nem chave (ver {@see Environment}). `Sandbox` + * é deprecated e emite E_USER_DEPRECATED. * @param int $timeout Default per-request timeout in seconds. * @param RetryPolicy $retry Retry behavior for transient failures. * @param Transport|null $transport Override the default cURL transport. Null = use default. @@ -57,6 +59,15 @@ public function __construct( if ($timeout <= 0) { throw new InvalidRequestException('Nfe\\Config: timeout must be positive.'); } + if ($environment === Environment::Sandbox) { + @trigger_error( + 'Nfe\Environment::Sandbox está deprecated e NÃO isola tráfego: não existe host ' + . 'sandbox na plataforma NFE.io — toda request vai para produção. Para testar sem ' + . 'efeito fiscal, use uma chave de conta de desenvolvimento e uma empresa com ' + . 'environment = Development. O case será removido na próxima major.', + E_USER_DEPRECATED, + ); + } } /** diff --git a/src/Environment.php b/src/Environment.php index 539f4eb..0f87ca7 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -5,13 +5,26 @@ namespace Nfe; /** - * Target environment for the NFE.io API. + * Declarative environment metadata for the NFE.io API. * - * Selection drives base URL routing in {@see Config::baseUrlForApi()}. - * Sandbox traffic is rate-limited and isolated from production data. + * **This enum does not route traffic.** The NFE.io platform has no sandbox + * host: every request goes to the production hosts regardless of this value + * (see {@see Config::baseUrlForApi()}, which never consults it). Isolation on + * NFE.io comes from the **API key scope** (use a development-account key) and + * from the **company's environment** (`company.environment = Development`), + * never from a subdomain. */ enum Environment: string { case Production = 'production'; + + /** + * @deprecated Não há host sandbox na plataforma — selecionar `Sandbox` + * NUNCA isolou tráfego (toda request vai para produção). Para + * testar sem efeito fiscal, use uma chave de conta de + * desenvolvimento e uma empresa com + * `company.environment = Development`. Será removido na + * próxima major. + */ case Sandbox = 'sandbox'; } diff --git a/src/Version.php b/src/Version.php index 2bf22f2..ad4690b 100644 --- a/src/Version.php +++ b/src/Version.php @@ -12,5 +12,5 @@ */ final class Version { - public const CURRENT = '3.3.1'; + public const CURRENT = '3.4.0'; } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index a674392..172d062 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -23,7 +23,7 @@ }); it('accepts a full Config object', function (): void { - $config = new Config(apiKey: 'k', environment: Environment::Sandbox, timeout: 120); + $config = new Config(apiKey: 'k', timeout: 120); $client = new Client(config: $config); expect($client->config)->toBe($config); expect($client->config->timeout)->toBe(120); diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 064e813..56e06f6 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -2,7 +2,12 @@ declare(strict_types=1); +use Nfe\Client; +use Nfe\Config; use Nfe\Environment; +use Nfe\Http\Response; +use Nfe\Http\RetryPolicy; +use Nfe\Tests\Support\MockTransport; it('has exactly two cases', function (): void { expect(Environment::cases())->toHaveCount(2); @@ -12,3 +17,49 @@ expect(Environment::Production->value)->toBe('production'); expect(Environment::Sandbox->value)->toBe('sandbox'); }); + +it('selecting Sandbox triggers E_USER_DEPRECATED; Production does not', function (): void { + $deprecations = []; + set_error_handler(function (int $errno, string $msg) use (&$deprecations): bool { + $deprecations[] = $msg; + return true; + }, E_USER_DEPRECATED); + + try { + new Config(apiKey: 'k', environment: Environment::Production); + expect($deprecations)->toBeEmpty(); + + new Config(apiKey: 'k', environment: Environment::Sandbox); + expect($deprecations)->toHaveCount(1); + expect($deprecations[0])->toContain('não existe host'); + expect($deprecations[0])->toContain('Development'); + } finally { + restore_error_handler(); + } +}); + +it('Sandbox and Production emit identical URLs (no host routing by environment)', function (): void { + // Pina a ausência de roteamento: não há host sandbox na plataforma + // (confirmado no Node e na spec client-core) — o isolamento real é por + // chave e por company.environment. + $urls = []; + set_error_handler(fn(): bool => true, E_USER_DEPRECATED); // o aviso é coberto pelo teste acima + try { + foreach ([Environment::Production, Environment::Sandbox] as $env) { + $mock = (new MockTransport())->push(new Response(200, [], '{"companies":[]}')); + $client = new Client(config: new Config( + apiKey: 'k', + environment: $env, + retry: RetryPolicy::none(), + transport: $mock, + )); + $client->companies->list(); + $urls[] = $mock->lastRequest()?->url(); + } + } finally { + restore_error_handler(); + } + + expect($urls[0])->toBe($urls[1]); + expect($urls[0])->toStartWith('https://api.nfe.io'); +});