From c2993fe3980102a1e4dbfa98d826fe917151aa03 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Sun, 4 Oct 2026 22:50:56 +0000
Subject: [PATCH] test: validate SEFIN event XML against official schema
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
docs/sefin-events.md | 31 +
.../schemas/nfse/1.01/pedRegEvento_v1.01.xsd | 15 +
.../schemas/nfse/1.01/tiposEventos_v1.01.xsd | 780 ++++++++++++++++++
src/Xml/EventSchemaValidator.php | 95 +++
.../Xml/OfficialEventSchemaValidationTest.php | 87 ++
5 files changed, 1008 insertions(+)
create mode 100644 docs/sefin-events.md
create mode 100644 references/schemas/nfse/1.01/pedRegEvento_v1.01.xsd
create mode 100644 references/schemas/nfse/1.01/tiposEventos_v1.01.xsd
create mode 100644 src/Xml/EventSchemaValidator.php
create mode 100644 tests/Unit/Xml/OfficialEventSchemaValidationTest.php
diff --git a/docs/sefin-events.md b/docs/sefin-events.md
new file mode 100644
index 0000000..bb85a72
--- /dev/null
+++ b/docs/sefin-events.md
@@ -0,0 +1,31 @@
+
+
+# SEFIN event endpoint contract
+
+The contributor manual currently documents these event endpoints:
+
+| Operation | Endpoint | nfse-php status |
+| --- | --- | --- |
+| Register signed event | `POST /nfse/{chaveAcesso}/eventos` | Supported by `EventRegistrationInterface::registerEventXml()`; cancellation uses this primitive. |
+| List all events | `GET /nfse/{chaveAcesso}/eventos` | Not enabled by default. |
+| List events by type | `GET /nfse/{chaveAcesso}/eventos/{tipoEvento}` | Not enabled by default. |
+| Query one event | `GET /nfse/{chaveAcesso}/eventos/{tipoEvento}/{numSeqEvento}` | Supported by `EventLookupInterface::queryEvent()`. |
+
+Source: Sistema Nacional NFS-e, current production contributor API manual.
+
+The production/restricted Swagger snapshot reviewed during implementation exposed the POST
+operation and the fully-qualified GET query, but did not expose the base or type-only GET
+operations. Because mutating or querying a fiscal API from an undocumented executable
+contract is an interoperability risk, nfse-php intentionally does not guess those two routes.
+
+When an authoritative executable contract exposes them, they can be added without changing
+the existing SEFIN/ADN separation.
+
+## Event XML
+
+Event registration uses the official `pedRegEvento_v1.01.xsd` contract and
+`tiposEventos_v1.01.xsd`. The repository validates cancellation/event-registration XML
+locally with `EventSchemaValidator`; schema validation never downloads external resources.
diff --git a/references/schemas/nfse/1.01/pedRegEvento_v1.01.xsd b/references/schemas/nfse/1.01/pedRegEvento_v1.01.xsd
new file mode 100644
index 0000000..d8d3db9
--- /dev/null
+++ b/references/schemas/nfse/1.01/pedRegEvento_v1.01.xsd
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Schema XML do Pedido de Registro de Eventos
+
+
+
\ No newline at end of file
diff --git a/references/schemas/nfse/1.01/tiposEventos_v1.01.xsd b/references/schemas/nfse/1.01/tiposEventos_v1.01.xsd
new file mode 100644
index 0000000..1a56868
--- /dev/null
+++ b/references/schemas/nfse/1.01/tiposEventos_v1.01.xsd
@@ -0,0 +1,780 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Versão do aplicativo que gerou o evento
+
+
+
+
+
+ Ambiente gerador do evento:
+ 1 - Sistema próprio do município;
+ 2 - Sefin Nacional NFS-e;
+ 3 - ADN NFS-e;
+
+
+
+
+
+
+ Número sequencial do evento para o mesmo tipo de evento.
+ Para os eventos que ocorrem somente uma vez, como é o caso do cancelamento, o nSeqEvento = 001.
+ Para os eventos que possam existir mais de um evento do mesmo tipo o ambiente gerador deverá numerar de forma sequencial.
+
+
+
+
+
+
+ Data/Hora do registro do evento.
+ Data e hora no formato UTC (Universal Coordinated Time): AAAA-MM-DDThh:mm:ssTZD
+
+
+
+
+
+ Número sequencial do documento gerado por ambiente gerador de DFSe do município
+
+
+
+
+ Leiaute do pedido de registro do evento gerado pelo autor do evento
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tipo de ambiente:
+ 1 - Produção;
+ 2 - Homologação;
+
+
+
+
+
+ Versão do aplicativo que gerou o pedido de registro de evento
+
+
+
+
+
+ Data e hora do evento no formato AAAA-MM-DDThh:mm:ssTZD (UTC - Universal Coordinated Time, onde TZD pode ser -02:00 (Fernando de Noronha), -03:00 (Brasília) ou -04:00 (Manaus), no horário de verão serão -01:00, -02:00 e -03:00. Ex.: 2010-08-19T13:00:15-03:00.
+
+
+
+
+
+ Identificação do Autor do Pedido de Evento
+
+
+
+
+ Número de inscrição federal (CNPJ) do autor do evento.
+ CNPJ do autor do evento (parte interessada ou pessoa que figure na NFS-e.
+ O autor do evento não é o procurador)
+
+
+
+
+
+
+ Número de inscrição federal (CPF) do autor do evento.
+ CPF do autor do evento (parte interessada ou pessoa que figure na NFS-e como prestador, tomador, intermediário.
+ O autor do evento poderá ser o procurador)
+
+
+
+
+
+
+ Identificador da NFS-e à qual o evento será vinculado
+
+
+
+
+
+ Evento de cancelamento
+
+
+
+
+ Evento de cancelamento por substituição
+
+
+
+
+ Solicitação de Análise Fiscal para Cancelamento de NFS-e
+
+
+
+
+ Cancelamento de NFS-e Deferido por Análise Fiscal
+
+
+
+
+ Cancelamento de NFS-e Indeferido por Análise Fiscal
+
+
+
+
+
+ Confirmação do Prestador
+
+
+
+
+ Confirmação do Tomador
+
+
+
+
+ Confirmação do Intermediário
+
+
+
+
+ Confirmação Tácita
+
+
+
+
+ Rejeição do Prestador
+
+
+
+
+ Rejeição do Tomador
+
+
+
+
+ Rejeição do Intermediário
+
+
+
+
+ Anulação da Rejeição
+
+
+
+
+
+ Cancelamento de NFS-e por Ofício
+
+
+
+
+ Bloqueio de NFS-e por Ofício
+
+
+
+
+ Desbloqueio de NFS-e por Ofício
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do Evento: Descrição do evento: "Cancelamento de NFS-e".
+
+
+
+
+
+
+
+
+
+
+
+
+ Código de justificativa de cancelamento:
+ 1 - Erro na Emissão;
+ 2 - Serviço não Prestado;
+ 9 - Outros;
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do Evento: Descrição do evento: "Cancelamento de NFS-e por Substituição".
+
+
+
+
+
+
+
+
+
+
+
+
+ Código de justificativa de cancelamento substituição:
+ 01 - Desenquadramento de NFS-e do Simples Nacional;
+ 02 - Enquadramento de NFS-e no Simples Nacional;
+ 03 - Inclusão Retroativa de Imunidade/Isenção para NFS-e;
+ 04 - Exclusão Retroativa de Imunidade/Isenção para NFS-e;
+ 05 - Rejeição de NFS-e pelo tomador ou pelo intermediário se responsável pelo recolhimento do tributo;
+ 99 - Outros;
+ Obtido do campo da DPS "DPS/infDPS/subst/cMotivo"
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento.
+ Obtido do campo da DPS "DPS/infDPS/subst/xMotivo".
+
+
+
+
+
+ Chave de Acesso da NFS-e substituta
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Solicitação de Análise Fiscal para Cancelamento de NFS-e"
+
+
+
+
+
+
+
+
+
+
+
+
+ Código do motivo da solicitação de análise fiscal para cancelamento de NFS-e:
+ 1 - Erro na Emissão;
+ 2 - Serviço não Prestado;
+ 9 - Outros;
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Cancelamento de NFS-e Deferido por Análise Fiscal"
+
+
+
+
+
+
+
+
+
+
+
+
+ CPF do agente da administração tributária municipal que efetuou o deferimento da solicitação de análise fiscal para cancelamento de NFS-e.
+
+
+
+
+
+
+ Número do processo administrativo municipal vinculado à solicitação de análise fiscal para cancelamento de NFS-e.
+
+
+
+
+
+
+ Resposta da solicitação de análise fiscal para cancelamento de NFS-e:
+ 1 - Cancelamento de NFS-e Deferido.
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Cancelamento de NFS-e Indeferido por Análise Fiscal".
+
+
+
+
+
+
+
+
+
+
+
+
+ CPF do agente da administração tributária municipal que efetuou o indeferimento da solicitação de análise fiscal para cancelamento de NFS-e.
+
+
+
+
+
+
+ Número do processo administrativo municipal vinculado à solicitação de análise fiscal para cancelamento de NFS-e.
+
+
+
+
+
+
+ Resposta da solicitação de análise fiscal para cancelamento de NFS-e:
+ 1 - Cancelamento de NFS-e Indeferido;
+ 2 - Cancelamento de NFS-e Indeferido Sem Análise de Mérito.
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Confirmação do Prestador".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Confirmação do Tomador".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Confirmação do Intermediário".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Confirmação Tácita".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Rejeição do Prestador".
+
+
+
+
+
+
+
+
+
+
+
+
+ Motivo da Rejeição da NFS-e:
+ 1 - NFS-e em duplicidade;
+ 2 - NFS-e já emitida pelo tomador;
+ 3 - Não ocorrência do fato gerador;
+ 4 - Erro quanto a responsabilidade tributária;
+ 5 - Erro quanto ao valor do serviço, valor das deduções ou serviço prestado ou data do fato gerador;
+ 9 - Outros;
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Rejeição do Tomador".
+
+
+
+
+
+
+
+
+
+
+
+
+ Motivo da Rejeição da NFS-e:
+ 1 - NFS-e em duplicidade;
+ 2 - NFS-e já emitida pelo tomador;
+ 3 - Não ocorrência do fato gerador;
+ 4 - Erro quanto a responsabilidade tributária;
+ 5 - Erro quanto ao valor do serviço, valor das deduções ou serviço prestado ou data do fato gerador;
+ 9 - Outros;
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Rejeição do Intermediário".
+
+
+
+
+
+
+
+
+
+
+
+
+ Motivo da Rejeição da NFS-e:
+ 1 - NFS-e em duplicidade;
+ 2 - NFS-e já emitida pelo tomador;
+ 3 - Não ocorrência do fato gerador;
+ 4 - Erro quanto a responsabilidade tributária;
+ 5 - Erro quanto ao valor do serviço, valor das deduções ou serviço prestado ou data do fato gerador;
+ 9 - Outros;
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Manifestação de NFS-e - Anulação da Rejeição".
+
+
+
+
+
+
+
+
+
+
+
+
+ CPF do agente da administração tributária municipal que efetuou o anulação da manifestação de rejeição da NFS-e
+
+
+
+
+
+
+ Referência ao "id" do Evento de Manifestação de NFS-e - Rejeição, que originou o presente evento de anulação
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Cancelamento de NFS-e por Ofício"
+
+
+
+
+
+
+
+
+
+
+
+
+ CPF do agente da administração tributária municipal que efetuou o cancelamento por ofício de NFS-e
+
+
+
+
+
+
+ Número do processo administrativo municipal vinculado ao cancelamento de NFS-e por ofício
+
+
+
+
+
+
+ Descrição para explicitar o motivo do processo administrativo municipal indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Bloqueio de NFS-e por Ofício".
+
+
+
+
+
+
+
+
+
+
+
+
+ CPF do agente da administração tributária municipal que efetuou o bloqueio de NFS-e por ofício
+
+
+
+
+
+
+ Eventos que podem ser escolhidos pelo município emissor para serem rejeitados após emissão e vinculação do evento de bloqueio por ofício em uma NFS-e:
+ e101101 - Cancelamento de NFS-e;
+ e105102 - Cancelamento de NFS-e por Substituição;
+ e105104 - Cancelamento de NFS-e Deferido por Análise Fiscal;
+ e105105 - Cancelamento de NFS-e Indeferido por Análise Fiscal;
+ e305101 - Cancelamento de NFS-e por Ofício;
+
+
+
+
+
+
+ Descrição para explicitar o motivo indicado neste evento
+
+
+
+
+
+
+
+
+
+
+
+
+ Descrição do evento: "Desbloqueio de NFS-e por Ofício".
+
+
+
+
+
+
+
+
+
+
+
+
+ CPF do agente da administração tributária municipal que efetuou o desbloqueio de NFS-e por ofício
+
+
+
+
+
+
+ Referência ao "id" do "Bloqueio de ofício" que originou o presente evento de desbloqueio
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Xml/EventSchemaValidator.php b/src/Xml/EventSchemaValidator.php
new file mode 100644
index 0000000..fe3c0f6
--- /dev/null
+++ b/src/Xml/EventSchemaValidator.php
@@ -0,0 +1,95 @@
+ */
+ public const SUPPORTED_SCHEMA_VERSIONS = [
+ self::SCHEMA_VERSION,
+ ];
+
+ public function __construct(
+ private readonly ?string $schemaPath = null,
+ private readonly string $schemaVersion = self::SCHEMA_VERSION,
+ ) {
+ if (!in_array($this->schemaVersion, self::SUPPORTED_SCHEMA_VERSIONS, true)) {
+ throw new \InvalidArgumentException(
+ 'Unsupported NFS-e event schema version: ' . $this->schemaVersion
+ . '. Supported versions: ' . implode(', ', self::SUPPORTED_SCHEMA_VERSIONS),
+ );
+ }
+ }
+
+ /**
+ * @return list Validation errors. An empty list means valid XML.
+ */
+ public function validate(string $xml): array
+ {
+ $previousUseErrors = libxml_use_internal_errors(true);
+ libxml_clear_errors();
+
+ try {
+ $document = new \DOMDocument();
+
+ if (!$document->loadXML($xml, LIBXML_NONET)) {
+ return $this->collectErrors();
+ }
+
+ if ($document->schemaValidate($this->resolvedSchemaPath())) {
+ return [];
+ }
+
+ return $this->collectErrors();
+ } finally {
+ libxml_clear_errors();
+ libxml_use_internal_errors($previousUseErrors);
+ }
+ }
+
+ public function isValid(string $xml): bool
+ {
+ return $this->validate($xml) === [];
+ }
+
+ private function resolvedSchemaPath(): string
+ {
+ return $this->schemaPath
+ ?? dirname(__DIR__, 2)
+ . '/references/schemas/nfse/'
+ . $this->schemaVersion
+ . '/pedRegEvento_v'
+ . $this->schemaVersion
+ . '.xsd';
+ }
+
+ /**
+ * @return list
+ */
+ private function collectErrors(): array
+ {
+ $errors = [];
+
+ foreach (libxml_get_errors() as $error) {
+ $message = trim($error->message);
+
+ if ($error->line > 0) {
+ $message .= ' (line ' . $error->line . ')';
+ }
+
+ $errors[] = $message;
+ }
+
+ return $errors;
+ }
+}
diff --git a/tests/Unit/Xml/OfficialEventSchemaValidationTest.php b/tests/Unit/Xml/OfficialEventSchemaValidationTest.php
new file mode 100644
index 0000000..d9cfd58
--- /dev/null
+++ b/tests/Unit/Xml/OfficialEventSchemaValidationTest.php
@@ -0,0 +1,87 @@
+validator = new EventSchemaValidator();
+ }
+
+ public function testCancellationRequestMatchesOfficialSchema(): void
+ {
+ self::assertSame([], $this->validator->validate($this->cancellationXml()));
+ }
+
+ public function testValidatorRejectsUnsupportedSchemaVersionExplicitly(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Unsupported NFS-e event schema version: 9.99');
+
+ new EventSchemaValidator(schemaVersion: '9.99');
+ }
+
+ public function testValidatorRejectsUnknownEventElement(): void
+ {
+ $invalidXml = str_replace('e101101', 'e999999', $this->cancellationXml());
+
+ self::assertNotSame([], $this->validator->validate($invalidXml));
+ }
+
+ public function testValidatorRejectsWrongEventElementOrder(): void
+ {
+ $invalidXml = str_replace(
+ '1Erro de emissao confirmado pelo prestador',
+ 'Erro de emissao confirmado pelo prestador1',
+ $this->cancellationXml(),
+ );
+
+ self::assertNotSame([], $this->validator->validate($invalidXml));
+ }
+
+ public function testMalformedXmlDoesNotResolveExternalNetworkResources(): void
+ {
+ $xml = '';
+
+ self::assertNotSame([], $this->validator->validate($xml));
+ }
+
+ private function cancellationXml(): string
+ {
+ $accessKey = str_repeat('1', 50);
+
+ return ''
+ . ''
+ . ''
+ . '2'
+ . 'nfse-php-test'
+ . '2026-10-04T12:00:00-03:00'
+ . '11222333000181'
+ . '' . $accessKey . ''
+ . ''
+ . 'Cancelamento de NFS-e'
+ . '1'
+ . 'Erro de emissao confirmado pelo prestador'
+ . ''
+ . ''
+ . '';
+ }
+}