Skip to content

Commit b3ba884

Browse files
authored
Merge pull request #16 from LibreCodeCoop/fix/cancelamento-via-evento
fix: send NFS-e cancellation as evento request
2 parents ae8519f + 0932f18 commit b3ba884

File tree

2 files changed

+77
-13
lines changed

2 files changed

+77
-13
lines changed

src/Http/NfseClient.php

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,19 @@ public function query(string $chaveAcesso): ReceiptData
8080

8181
public function cancel(string $chaveAcesso, string $motivo): bool
8282
{
83-
[$httpStatus, $body] = $this->delete('/dps/' . $chaveAcesso, $motivo);
83+
$eventoXml = $this->buildCancelEventXml($chaveAcesso, $motivo);
84+
$signedEventoXml = $this->signer->sign($eventoXml, $this->cert->cnpj);
85+
86+
$compressedEventoXml = gzencode($signedEventoXml);
87+
88+
if ($compressedEventoXml === false) {
89+
throw new NetworkException('Failed to compress cancellation event XML payload before transmission.');
90+
}
91+
92+
[$httpStatus, $body] = $this->postEvento(
93+
'/nfse/' . $chaveAcesso . '/eventos',
94+
base64_encode($compressedEventoXml),
95+
);
8496

8597
if ($httpStatus >= 400) {
8698
throw new CancellationException(
@@ -146,12 +158,15 @@ private function get(string $path): array
146158
/**
147159
* @return array{int, array<string, mixed>}
148160
*/
149-
private function delete(string $path, string $motivo): array
161+
private function postEvento(string $path, string $eventoXmlGZipB64): array
150162
{
151-
$payload = json_encode(['motivo' => $motivo], JSON_THROW_ON_ERROR);
163+
$payload = json_encode([
164+
'pedidoRegistroEventoXmlGZipB64' => $eventoXmlGZipB64,
165+
], JSON_THROW_ON_ERROR);
166+
152167
$context = stream_context_create([
153168
'http' => [
154-
'method' => 'DELETE',
169+
'method' => 'POST',
155170
'header' => "Content-Type: application/json\r\nAccept: application/json\r\n",
156171
'content' => $payload,
157172
'ignore_errors' => true,
@@ -162,6 +177,34 @@ private function delete(string $path, string $motivo): array
162177
return $this->fetchAndDecode($path, $context);
163178
}
164179

180+
private function buildCancelEventXml(string $chaveAcesso, string $motivo): string
181+
{
182+
$doc = new \DOMDocument('1.0', 'UTF-8');
183+
$doc->formatOutput = false;
184+
185+
$root = $doc->createElementNS('http://www.sped.fazenda.gov.br/nfse', 'pedRegEvento');
186+
$root->setAttribute('versao', '1.01');
187+
$doc->appendChild($root);
188+
189+
$infPedReg = $doc->createElement('infPedReg');
190+
$infPedReg->setAttribute('Id', 'PRE' . $chaveAcesso . '101101');
191+
$root->appendChild($infPedReg);
192+
193+
$infPedReg->appendChild($doc->createElement('tpAmb', $this->environment->sandboxMode ? '2' : '1'));
194+
$infPedReg->appendChild($doc->createElement('verAplic', 'akaunting-nfse'));
195+
$infPedReg->appendChild($doc->createElement('dhEvento', (new \DateTimeImmutable())->format('Y-m-d\\TH:i:sP')));
196+
$infPedReg->appendChild($doc->createElement('CNPJAutor', $this->cert->cnpj));
197+
$infPedReg->appendChild($doc->createElement('chNFSe', $chaveAcesso));
198+
199+
$e101101 = $doc->createElement('e101101');
200+
$e101101->appendChild($doc->createElement('xDesc', 'Cancelamento de NFS-e'));
201+
$e101101->appendChild($doc->createElement('cMotivo', '1'));
202+
$e101101->appendChild($doc->createElement('xMotivo', $motivo));
203+
$infPedReg->appendChild($e101101);
204+
205+
return $doc->saveXML($doc->documentElement) ?: '';
206+
}
207+
165208
/**
166209
* @return array<string, bool|string>
167210
*/

tests/Unit/Http/NfseClientTest.php

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public function testQueryReturnsReceiptDataOnSuccess(): void
136136
new Response($payload, ['Content-Type' => 'application/json'], 200)
137137
);
138138

139-
$client = $this->makeClient();
139+
$client = $this->makeClient($this->signer);
140140

141141
$receipt = $client->query('xyz-456');
142142

@@ -146,13 +146,34 @@ public function testQueryReturnsReceiptDataOnSuccess(): void
146146
public function testCancelReturnsTrueOnSuccess(): void
147147
{
148148
self::$server->setResponseOfPath(
149-
'/SefinNacional/dps/abc-123',
149+
'/SefinNacional/nfse/abc-123/eventos',
150150
new Response('{}', ['Content-Type' => 'application/json'], 200)
151151
);
152152

153-
$client = $this->makeClient();
153+
$client = $this->makeClient($this->signer);
154154

155155
self::assertTrue($client->cancel('abc-123', 'Cancelamento a pedido do tomador'));
156+
157+
$request = self::$server->getLastRequest();
158+
self::assertNotNull($request);
159+
self::assertSame('POST', $request->getRequestMethod());
160+
self::assertSame('/SefinNacional/nfse/abc-123/eventos', $request->getRequestUri());
161+
162+
$payload = json_decode($request->getInput(), true, 512, JSON_THROW_ON_ERROR);
163+
self::assertIsArray($payload);
164+
self::assertArrayHasKey('pedidoRegistroEventoXmlGZipB64', $payload);
165+
166+
$compressedXml = base64_decode((string) $payload['pedidoRegistroEventoXmlGZipB64'], true);
167+
self::assertNotFalse($compressedXml);
168+
169+
$eventoXml = gzdecode($compressedXml);
170+
self::assertNotFalse($eventoXml);
171+
self::assertStringContainsString('<pedRegEvento', $eventoXml);
172+
self::assertStringContainsString('<chNFSe>abc-123</chNFSe>', $eventoXml);
173+
self::assertStringContainsString('<e101101>', $eventoXml);
174+
self::assertStringContainsString('<xDesc>Cancelamento de NFS-e</xDesc>', $eventoXml);
175+
self::assertStringContainsString('<cMotivo>1</cMotivo>', $eventoXml);
176+
self::assertStringContainsString('<xMotivo>Cancelamento a pedido do tomador</xMotivo>', $eventoXml);
156177
}
157178

158179
// -------------------------------------------------------------------------
@@ -202,7 +223,7 @@ public function testQueryThrowsQueryExceptionWhenGatewayReturnsError(): void
202223
new Response('{"error":"not found"}', ['Content-Type' => 'application/json'], 404),
203224
);
204225

205-
$client = $this->makeClient();
226+
$client = $this->makeClient($this->signer);
206227

207228
$this->expectException(QueryException::class);
208229
$client->query('missing-key');
@@ -215,7 +236,7 @@ public function testQueryExceptionCarriesErrorCodeAndHttpStatus(): void
215236
new Response('{"error":"not found"}', ['Content-Type' => 'application/json'], 404),
216237
);
217238

218-
$client = $this->makeClient();
239+
$client = $this->makeClient($this->signer);
219240

220241
try {
221242
$client->query('missing-key');
@@ -229,11 +250,11 @@ public function testQueryExceptionCarriesErrorCodeAndHttpStatus(): void
229250
public function testCancelThrowsCancellationExceptionWhenGatewayReturnsError(): void
230251
{
231252
self::$server->setResponseOfPath(
232-
'/SefinNacional/dps/blocked-key',
253+
'/SefinNacional/nfse/blocked-key/eventos',
233254
new Response('{"error":"cannot cancel"}', ['Content-Type' => 'application/json'], 409),
234255
);
235256

236-
$client = $this->makeClient();
257+
$client = $this->makeClient($this->signer);
237258

238259
$this->expectException(CancellationException::class);
239260
$client->cancel('blocked-key', 'a pedido do tomador');
@@ -242,11 +263,11 @@ public function testCancelThrowsCancellationExceptionWhenGatewayReturnsError():
242263
public function testCancellationExceptionCarriesErrorCodeAndHttpStatus(): void
243264
{
244265
self::$server->setResponseOfPath(
245-
'/SefinNacional/dps/blocked-key',
266+
'/SefinNacional/nfse/blocked-key/eventos',
246267
new Response('{"error":"cannot cancel"}', ['Content-Type' => 'application/json'], 409),
247268
);
248269

249-
$client = $this->makeClient();
270+
$client = $this->makeClient($this->signer);
250271

251272
try {
252273
$client->cancel('blocked-key', 'a pedido do tomador');

0 commit comments

Comments
 (0)