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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
29 changes: 20 additions & 9 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion skills/nfeio-php-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down
13 changes: 12 additions & 1 deletion src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
);
}
}

/**
Expand Down
19 changes: 16 additions & 3 deletions src/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
2 changes: 1 addition & 1 deletion src/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
*/
final class Version
{
public const CURRENT = '3.3.1';
public const CURRENT = '3.4.0';
}
2 changes: 1 addition & 1 deletion tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
51 changes: 51 additions & 0 deletions tests/EnvironmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
});