Skip to content

Commit 1e4a7bf

Browse files
authored
fix(inbound): migra os 20 métodos de inbound/CT-e para as rotas reais /inbound/… (#30)
Os dois resources do domínio de documentos de entrada nunca funcionaram na v3: as rotas usadas (/productinvoices/received/…, /productinvoices/inbound, /cte/…) não existem na API — 404 de rota ou colisão com /productinvoices/{id} (sondado ao vivo 2026-07-29/30). Migra tudo para o contrato /inbound/… da consulta-dfe-distribuicao-v2 (spec canônica), mantendo nomes e assinaturas públicas dos 20 métodos: - config NF-e/CT-e: POST|GET|DELETE …/inbound/{productinvoices|transportationinvoices} (habilitar era PUT, agora POST conforme a spec) - consultas: rotas genéricas …/inbound/{key}… e específicas …/inbound/productinvoices/{key}… - manifest(): POST …/inbound/{key}/manifest?tpEvent={código} — sondado: o binder só aceita código numérico SEFAZ; literais legados do SDK são mapeados (Confirmation→210200, Acknowledgement→210210, Unknown→210220, Refused→210240) - reprocessWebhook(): POST …/inbound/productinvoices/{key_or_nsu}/processwebhook, aceitando NSU via novo IdValidator::accessKeyOrNsu() Testes: verbo+path pinados por método (datasets), InboundSpecAlignmentTest (paths↔spec DF-e v2, rotas mortas ausentes, tpEvent integer, assinaturas dos 20 métodos por reflexão). Revalidado ao vivo com o código novo: settings e chaves falsas retornam erros de domínio, nunca 404 de rota. OpenSpec: fix-inbound-routes
1 parent bc17b51 commit 1e4a7bf

11 files changed

Lines changed: 429 additions & 71 deletions

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,38 @@ e este projeto segue [Versionamento Semântico](https://semver.org/lang/pt-BR/sp
77

88
## [Unreleased]
99

10+
### Corrigido
11+
12+
- **Domínio de documentos de entrada inteiro** (`fix-inbound-routes`): os 20 métodos
13+
de `InboundProductInvoicesResource` (13) e `TransportationInvoicesResource` (7)
14+
usavam rotas (`/productinvoices/received/…`, `/productinvoices/inbound`, `/cte/…`)
15+
que **nunca existiram na API** — desde a v3.0, todo o domínio retornava 404 de rota
16+
(ou colidia com `/productinvoices/{id}`). Sondado ao vivo em 2026-07-29/30. Todas as
17+
rotas migraram para o contrato real `/inbound/…` de
18+
`openapi/consulta-dfe-distribuicao-v2.yaml`, **mantendo nomes e assinaturas dos 20
19+
métodos** (pinado por teste de reflexão):
20+
- Configuração: `POST|GET|DELETE /v2/companies/{id}/inbound/productinvoices`
21+
(NF-e) e `…/inbound/transportationinvoices` (CT-e) — habilitar agora é `POST`
22+
(era `PUT`).
23+
- Consultas por chave: rotas genéricas `…/inbound/{accessKey}[/xml|/pdf|/events/…]`
24+
e específicas `…/inbound/productinvoices/{accessKey}[/json|/events/…]`.
25+
- `manifest()`: `POST …/inbound/{accessKey}/manifest?tpEvent={código}` — a API só
26+
aceita o código numérico SEFAZ (sondado); o SDK aceita o código direto ou os
27+
literais legados (`Confirmation`→210200, `Acknowledgement`→210210,
28+
`Unknown`→210220, `Refused`→210240).
29+
- `reprocessWebhook()`: `POST …/inbound/productinvoices/{key_or_nsu}/processwebhook`
30+
— aceita chave de 44 dígitos **ou** NSU (1–15 dígitos).
31+
32+
### Adicionado
33+
34+
- `IdValidator::accessKeyOrNsu()` — normaliza chave de acesso (44 dígitos) ou NSU
35+
(1–15 dígitos) para o reprocessamento de webhook.
36+
- Teste de alinhamento `InboundSpecAlignmentTest`: amarra as 20 rotas à spec DF-e v2
37+
(verbo+path), pina a ausência dos esquemas de rota mortos e o tipo `integer` de
38+
`tpEvent`; unit tests com verbo+path pinados por método. Rotas validadas ao vivo
39+
com o SDK corrigido em 2026-07-30 (erros de domínio — settings/chave falsa/NSU —
40+
nunca 404 de rota).
41+
1042
## [3.3.1] — 2026-07-30
1143

1244
### Corrigido

docs/recursos/inbound-product-invoices.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ dados — sempre usa a chave principal.
3333
| `getEventXml($companyId, $accessKey, $eventKey)` | XML de um evento. | `string` |
3434
| `getPdf($companyId, $accessKey)` | DANFE em PDF. | `string` |
3535
| `getJson($companyId, $accessKey)` | Representação JSON do documento. | `array` |
36-
| `manifest($companyId, $accessKey, $manifestType, $data = [])` | Manifestação do destinatário. | `array` |
37-
| `reprocessWebhook($companyId, $accessKey)` | Reenvia o webhook do documento. | `array` |
36+
| `manifest($companyId, $accessKey, $manifestType, $data = [])` | Manifestação do destinatário (`$manifestType` = código SEFAZ ou literal, ver abaixo). | `array` |
37+
| `reprocessWebhook($companyId, $accessKey)` | Reenvia o webhook do documento; aceita a chave de 44 dígitos **ou** o NSU (1–15 dígitos). | `array` |
38+
39+
:::warning Corrigido na v3.4.0
40+
Até a v3.3.x este recurso apontava para rotas que **não existem** na API — todos
41+
os métodos retornavam 404/erro. A v3.4.0 migrou tudo para o contrato real
42+
`/inbound/…` (`consulta-dfe-distribuicao-v2`), mantendo nomes e assinaturas.
43+
:::
3844

3945
## Ativar a busca automática
4046

@@ -61,22 +67,37 @@ file_put_contents('entrada.pdf', $nfe->inboundProductInvoices->getPdf($companyId
6167

6268
## Manifestação do destinatário
6369

70+
A API espera o **código numérico SEFAZ** do evento (query `tpEvent`). O SDK
71+
aceita o código direto ou os literais equivalentes:
72+
73+
| Código | Literal aceito | Evento |
74+
|---|---|---|
75+
| `210200` | `Confirmation` | Confirmação da Operação |
76+
| `210210` | `Acknowledgement` | Ciência da Operação |
77+
| `210220` | `Unknown` | Desconhecimento da Operação |
78+
| `210240` | `Refused` | Operação não Realizada |
79+
6480
```php
65-
// Ex.: confirmar a operação
66-
$nfe->inboundProductInvoices->manifest($companyId, $accessKey, 'Confirmation');
81+
// Ex.: ciência da operação (código direto)
82+
$nfe->inboundProductInvoices->manifest($companyId, $accessKey, '210210');
6783

6884
// Ex.: desconhecer a operação, com justificativa no corpo
69-
$nfe->inboundProductInvoices->manifest($companyId, $accessKey, 'Ignorance', [
70-
'reason' => 'Operação não reconhecida',
85+
$nfe->inboundProductInvoices->manifest($companyId, $accessKey, 'Unknown', [
86+
'justification' => 'Operação não reconhecida',
7187
]);
7288
```
7389

90+
Qualquer outro valor lança `Nfe\Exception\InvalidRequestException` antes de
91+
qualquer HTTP.
92+
7493
## Reprocessar o webhook
7594

76-
Se a sua aplicação perdeu uma entrega, peça o reenvio do evento do documento:
95+
Se a sua aplicação perdeu uma entrega, peça o reenvio do evento do documento —
96+
pela chave de acesso ou pelo NSU:
7797

7898
```php
7999
$nfe->inboundProductInvoices->reprocessWebhook($companyId, $accessKey);
100+
$nfe->inboundProductInvoices->reprocessWebhook($companyId, '123456789'); // NSU
80101
```
81102

82103
## Próximos passos

docs/recursos/transportation-invoices.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ principal (não é família de dados).
3030
| `getEvent($companyId, $accessKey, $eventKey)` | Consulta um evento do CT-e. | `array` |
3131
| `downloadEventXml($companyId, $accessKey, $eventKey)` | XML de um evento. | `string` |
3232

33+
:::warning Corrigido na v3.4.0
34+
Até a v3.3.x este recurso apontava para rotas `/cte/…` que **não existem** na
35+
API — todos os métodos retornavam 404/erro. A v3.4.0 migrou tudo para o
36+
contrato real `/inbound/…` (`consulta-dfe-distribuicao-v2`), mantendo nomes e
37+
assinaturas.
38+
:::
39+
3340
## Habilitar o recebimento
3441

3542
```php

skills/nfeio-php-sdk/references/product-invoices-and-taxes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ downloadEventXml(string $companyId, string $accessKey, string $eventKey, ?Reques
9595
```
9696

9797
- **Does not use `dataApiKey`** — always the main `apiKey`.
98+
- Routes live under `/v2/companies/{id}/inbound/transportationinvoices` (config) and the generic `/inbound/{accessKey}…` (queries); `enable` is a POST. Fixed in v3.4.0 — before that, the resource hit dead `/cte/…` routes and every method 404'd.
9899

99100
## `$nfe->inboundProductInvoices` (Inbound NF-e / DFe distribution — Company-scoped)
100101

@@ -115,3 +116,6 @@ reprocessWebhook(string $companyId, string $accessKey, ?RequestOptions $options
115116
```
116117

117118
- `getXml`/`getEventXml`/`getPdf` return raw byte `string`. **Does not use `dataApiKey`** — always the main `apiKey`.
119+
- Routes live under `/v2/companies/{id}/inbound/…` (fixed in v3.4.0; the old `/productinvoices/received/…` and `/productinvoices/inbound` routes never existed on the API — every method 404'd until then). `enableAutoFetch` is a POST.
120+
- `manifest()` sends the SEFAZ numeric event code as query `tpEvent` (probed: the API rejects literals). Pass the code (`'210210'`) or a legacy literal, mapped as `Confirmation`→210200, `Acknowledgement`→210210, `Unknown`→210220, `Refused`→210240; anything else throws `InvalidRequestException` locally.
121+
- `reprocessWebhook()` accepts a 44-digit access key **or** a 1–15 digit NSU (`IdValidator::accessKeyOrNsu`), hitting `POST …/inbound/productinvoices/{key_or_nsu}/processwebhook`.

src/Resource/InboundProductInvoicesResource.php

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,23 @@ public function enableAutoFetch(
3737
?RequestOptions $options = null,
3838
): InboundSettings {
3939
$companyId = IdValidator::companyId($companyId);
40-
$response = $this->httpPut("/companies/{$companyId}/productinvoices/inbound", $data, $options);
40+
$response = $this->httpPost("/companies/{$companyId}/inbound/productinvoices", $data, $options);
4141

4242
return $this->hydrate(InboundSettings::class, $this->decodeBody($response->body));
4343
}
4444

4545
public function disableAutoFetch(string $companyId, ?RequestOptions $options = null): InboundSettings
4646
{
4747
$companyId = IdValidator::companyId($companyId);
48-
$response = $this->httpDelete("/companies/{$companyId}/productinvoices/inbound", $options);
48+
$response = $this->httpDelete("/companies/{$companyId}/inbound/productinvoices", $options);
4949

5050
return $this->hydrate(InboundSettings::class, $this->decodeBody($response->body));
5151
}
5252

5353
public function getSettings(string $companyId, ?RequestOptions $options = null): InboundSettings
5454
{
5555
$companyId = IdValidator::companyId($companyId);
56-
$response = $this->httpGet("/companies/{$companyId}/productinvoices/inbound", options: $options);
56+
$response = $this->httpGet("/companies/{$companyId}/inbound/productinvoices", options: $options);
5757

5858
return $this->hydrate(InboundSettings::class, $this->decodeBody($response->body));
5959
}
@@ -66,7 +66,7 @@ public function getDetails(string $companyId, string $accessKey, ?RequestOptions
6666
$companyId = IdValidator::companyId($companyId);
6767
$accessKey = IdValidator::accessKey($accessKey);
6868
$response = $this->httpGet(
69-
"/companies/{$companyId}/productinvoices/received/{$accessKey}",
69+
"/companies/{$companyId}/inbound/{$accessKey}",
7070
options: $options,
7171
);
7272

@@ -84,7 +84,7 @@ public function getProductInvoiceDetails(
8484
$companyId = IdValidator::companyId($companyId);
8585
$accessKey = IdValidator::accessKey($accessKey);
8686
$response = $this->httpGet(
87-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/productinvoice",
87+
"/companies/{$companyId}/inbound/productinvoices/{$accessKey}",
8888
options: $options,
8989
);
9090

@@ -104,7 +104,7 @@ public function getEventDetails(
104104
$accessKey = IdValidator::accessKey($accessKey);
105105
$eventKey = IdValidator::eventKey($eventKey);
106106
$response = $this->httpGet(
107-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/events/{$eventKey}",
107+
"/companies/{$companyId}/inbound/{$accessKey}/events/{$eventKey}",
108108
options: $options,
109109
);
110110

@@ -124,7 +124,7 @@ public function getProductInvoiceEventDetails(
124124
$accessKey = IdValidator::accessKey($accessKey);
125125
$eventKey = IdValidator::eventKey($eventKey);
126126
$response = $this->httpGet(
127-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/events/{$eventKey}/productinvoice",
127+
"/companies/{$companyId}/inbound/productinvoices/{$accessKey}/events/{$eventKey}",
128128
options: $options,
129129
);
130130

@@ -137,7 +137,7 @@ public function getXml(string $companyId, string $accessKey, ?RequestOptions $op
137137
$accessKey = IdValidator::accessKey($accessKey);
138138

139139
return $this->download(
140-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/xml",
140+
"/companies/{$companyId}/inbound/{$accessKey}/xml",
141141
options: $options,
142142
);
143143
}
@@ -153,7 +153,7 @@ public function getEventXml(
153153
$eventKey = IdValidator::eventKey($eventKey);
154154

155155
return $this->download(
156-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/events/{$eventKey}/xml",
156+
"/companies/{$companyId}/inbound/{$accessKey}/events/{$eventKey}/xml",
157157
options: $options,
158158
);
159159
}
@@ -164,7 +164,7 @@ public function getPdf(string $companyId, string $accessKey, ?RequestOptions $op
164164
$accessKey = IdValidator::accessKey($accessKey);
165165

166166
return $this->download(
167-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/pdf",
167+
"/companies/{$companyId}/inbound/{$accessKey}/pdf",
168168
options: $options,
169169
);
170170
}
@@ -177,15 +177,21 @@ public function getJson(string $companyId, string $accessKey, ?RequestOptions $o
177177
$companyId = IdValidator::companyId($companyId);
178178
$accessKey = IdValidator::accessKey($accessKey);
179179
$response = $this->httpGet(
180-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/json",
180+
"/companies/{$companyId}/inbound/productinvoices/{$accessKey}/json",
181181
options: $options,
182182
);
183183

184184
return $this->decodeBody($response->body);
185185
}
186186

187187
/**
188-
* Manifestar o destinatário (ciência, confirmação, desconhecimento, refutação).
188+
* Manifestar o destinatário (ciência, confirmação, desconhecimento, operação não realizada).
189+
*
190+
* A API espera o código numérico SEFAZ no query param `tpEvent` (sondado
191+
* 2026-07-30: literais são rejeitados pelo binder). Aceita o código direto
192+
* (`'210210'`) ou os literais legados deste SDK, mapeados assim:
193+
* `Confirmation`→210200, `Acknowledgement`→210210, `Unknown`→210220,
194+
* `Refused`→210240.
189195
*
190196
* @param array<string, mixed> $data Pode conter justification, etc. dependendo do tipo.
191197
* @return array<string, mixed>
@@ -199,11 +205,14 @@ public function manifest(
199205
): array {
200206
$companyId = IdValidator::companyId($companyId);
201207
$accessKey = IdValidator::accessKey($accessKey);
202-
if (trim($manifestType) === '') {
203-
throw new \Nfe\Exception\InvalidRequestException('manifestType é obrigatório (Confirmation/Acknowledgement/Unknown/Refused).');
208+
$tpEvent = self::MANIFEST_EVENT_CODES[trim($manifestType)] ?? trim($manifestType);
209+
if (preg_match('/^\d{6}$/', $tpEvent) !== 1) {
210+
throw new \Nfe\Exception\InvalidRequestException(
211+
'manifestType inválido: use o código SEFAZ de 6 dígitos (210200/210210/210220/210240) ou Confirmation/Acknowledgement/Unknown/Refused.',
212+
);
204213
}
205-
$response = $this->httpPut(
206-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/manifest/{$manifestType}",
214+
$response = $this->httpPost(
215+
"/companies/{$companyId}/inbound/{$accessKey}/manifest?" . http_build_query(['tpEvent' => $tpEvent]),
207216
$data,
208217
$options,
209218
);
@@ -212,7 +221,8 @@ public function manifest(
212221
}
213222

214223
/**
215-
* Reenvia o webhook para uma NF-e recebida.
224+
* Reenvia o webhook para uma NF-e recebida, identificada pela chave de
225+
* acesso (44 dígitos) OU pelo NSU (1–15 dígitos).
216226
*
217227
* @return array<string, mixed>
218228
*/
@@ -222,12 +232,20 @@ public function reprocessWebhook(
222232
?RequestOptions $options = null,
223233
): array {
224234
$companyId = IdValidator::companyId($companyId);
225-
$accessKey = IdValidator::accessKey($accessKey);
235+
$accessKey = IdValidator::accessKeyOrNsu($accessKey);
226236
$response = $this->httpPost(
227-
"/companies/{$companyId}/productinvoices/received/{$accessKey}/webhook/reprocess",
237+
"/companies/{$companyId}/inbound/productinvoices/{$accessKey}/processwebhook",
228238
options: $options,
229239
);
230240

231241
return $this->decodeBody($response->body);
232242
}
243+
244+
/** Literais legados do SDK → código numérico SEFAZ do evento de manifestação. */
245+
private const MANIFEST_EVENT_CODES = [
246+
'Confirmation' => '210200',
247+
'Acknowledgement' => '210210',
248+
'Unknown' => '210220',
249+
'Refused' => '210240',
250+
];
233251
}

src/Resource/TransportationInvoicesResource.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function enable(
4141
?RequestOptions $options = null,
4242
): InboundSettings {
4343
$companyId = IdValidator::companyId($companyId);
44-
$response = $this->httpPut("/companies/{$companyId}/cte/inbound", $data, $options);
44+
$response = $this->httpPost("/companies/{$companyId}/inbound/transportationinvoices", $data, $options);
4545
$payload = $this->decodeBody($response->body);
4646

4747
return $this->hydrate(InboundSettings::class, $payload);
@@ -50,7 +50,7 @@ public function enable(
5050
public function disable(string $companyId, ?RequestOptions $options = null): InboundSettings
5151
{
5252
$companyId = IdValidator::companyId($companyId);
53-
$response = $this->httpDelete("/companies/{$companyId}/cte/inbound", $options);
53+
$response = $this->httpDelete("/companies/{$companyId}/inbound/transportationinvoices", $options);
5454
$payload = $this->decodeBody($response->body);
5555

5656
return $this->hydrate(InboundSettings::class, $payload);
@@ -59,7 +59,7 @@ public function disable(string $companyId, ?RequestOptions $options = null): Inb
5959
public function getSettings(string $companyId, ?RequestOptions $options = null): InboundSettings
6060
{
6161
$companyId = IdValidator::companyId($companyId);
62-
$response = $this->httpGet("/companies/{$companyId}/cte/inbound", options: $options);
62+
$response = $this->httpGet("/companies/{$companyId}/inbound/transportationinvoices", options: $options);
6363
$payload = $this->decodeBody($response->body);
6464

6565
return $this->hydrate(InboundSettings::class, $payload);
@@ -77,7 +77,7 @@ public function retrieve(
7777
): array {
7878
$companyId = IdValidator::companyId($companyId);
7979
$accessKey = IdValidator::accessKey($accessKey);
80-
$response = $this->httpGet("/companies/{$companyId}/cte/{$accessKey}", options: $options);
80+
$response = $this->httpGet("/companies/{$companyId}/inbound/{$accessKey}", options: $options);
8181

8282
return $this->decodeBody($response->body);
8383
}
@@ -90,7 +90,7 @@ public function downloadXml(
9090
$companyId = IdValidator::companyId($companyId);
9191
$accessKey = IdValidator::accessKey($accessKey);
9292

93-
return $this->download("/companies/{$companyId}/cte/{$accessKey}/xml", options: $options);
93+
return $this->download("/companies/{$companyId}/inbound/{$accessKey}/xml", options: $options);
9494
}
9595

9696
/**
@@ -106,7 +106,7 @@ public function getEvent(
106106
$accessKey = IdValidator::accessKey($accessKey);
107107
$eventKey = IdValidator::eventKey($eventKey);
108108
$response = $this->httpGet(
109-
"/companies/{$companyId}/cte/{$accessKey}/events/{$eventKey}",
109+
"/companies/{$companyId}/inbound/{$accessKey}/events/{$eventKey}",
110110
options: $options,
111111
);
112112

@@ -124,7 +124,7 @@ public function downloadEventXml(
124124
$eventKey = IdValidator::eventKey($eventKey);
125125

126126
return $this->download(
127-
"/companies/{$companyId}/cte/{$accessKey}/events/{$eventKey}/xml",
127+
"/companies/{$companyId}/inbound/{$accessKey}/events/{$eventKey}/xml",
128128
options: $options,
129129
);
130130
}

src/Util/IdValidator.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,29 @@ public static function accessKey(string $value): string
8282
return $digits;
8383
}
8484

85+
/**
86+
* Normalise a 44-digit access key OR an NSU (1–15 digits).
87+
*
88+
* O endpoint de reprocessamento de webhook aceita `{access_key_or_nsu}`
89+
* (spec consulta-dfe-distribuicao-v2): 44 dígitos são tratados como chave
90+
* de acesso; qualquer outra contagem de 1 a 15 dígitos é aceita como NSU.
91+
*/
92+
public static function accessKeyOrNsu(string $value): string
93+
{
94+
$digits = preg_replace('/\D+/', '', $value) ?? '';
95+
$len = strlen($digits);
96+
if ($len === 44 || ($len >= 1 && $len <= 15)) {
97+
return $digits;
98+
}
99+
throw new InvalidRequestException(
100+
sprintf(
101+
'Chave de acesso ou NSU inválido: esperado 44 dígitos (chave) ou 1–15 dígitos (NSU), recebido %d (input: "%s").',
102+
$len,
103+
$value,
104+
),
105+
);
106+
}
107+
85108
/**
86109
* Normalise a CNPJ (14 digits). Strips punctuation; does not validate check digits.
87110
*/

0 commit comments

Comments
 (0)