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
65 changes: 65 additions & 0 deletions .github/workflows/nt009-evidence.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

name: NT009 Source Evidence

on:
pull_request:
paths:
- "tools/src/Nt009Inspector.php"
- "tools/bin/domains.php"
- "tools/tests/Nt009InspectorTest.php"
- ".github/workflows/nt009-evidence.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
optional-official-evidence:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up PHP 8.2
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2
with:
php-version: "8.2"
extensions: dom, mbstring, zip, gd, xml, xmlreader, xmlwriter, simplexml, fileinfo
coverage: none
- name: Install isolated domain tooling
run: composer --working-dir=vendor-bin/domains install --prefer-dist --no-progress --no-interaction
- name: Verify original XLSX hashes and compare official NT009 data
run: |
php tools/bin/domains.php audit \
--download-dir="${{ runner.temp }}/nt009" \
--report="${{ runner.temp }}/nt009-comparison.json"
- name: Verify all 40 published indicators match the versioned TSV
run: |
php -r '
require "vendor-bin/domains/vendor/autoload.php";
$generator = new LibreCodeCoop\NfsePHP\Tools\DomainTableGenerator();
$rows = $generator->indicatorRows(getenv("RUNNER_TEMP") . "/nt009/annex-vii-nt009.xlsx");
$path = "resources/domains/indicadores-operacao-ibscbs-v1.03.00.tsv";
$snapshot = array_values(array_filter(
explode("\n", trim(file_get_contents($path))),
static fn (string $line): bool => $line !== "" && !str_starts_with($line, "#")
));
$source = array_map(static fn (array $row): string => implode("\t", $row), $rows);
if (count($source) !== 40 || $source !== $snapshot) {
throw new UnexpectedValueException("Versioned Anexo VII differs from the official signed-off source");
}
echo "Official Anexo VII snapshot verified: 40/40 exact code/characteristic/location triplets\n";
'
- name: Retain full evidence for manual review
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: nt009-official-source-evidence
path: |
${{ runner.temp }}/nt009-comparison.json
${{ runner.temp }}/nt009/*.xlsx
if-no-files-found: warn
retention-days: 30
58 changes: 58 additions & 0 deletions docs/NT009-SOURCE-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!--
SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
SPDX-License-Identifier: AGPL-3.0-or-later
-->

# NT009 v1.01: inspect published contracts without activating them

The official [RTC portal](https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/rtc)
publishes **Anexo VI v1.04.01** (DPS/NFS-e layout and fiscal business rules)
and **Anexo VII v1.03.00** (IBS/CBS operation indicators). The publication
does not by itself establish a deployed production schema or effective date.

A strictly read-only source review uses PHP/PhpSpreadsheet from the isolated
`vendor-bin/domains` Composer project:

```sh
composer domains:install
composer domains:audit -- --download-dir=/tmp/nfse-nt009 --report=/tmp/nt009-report.json
# Or use previously downloaded, independently verified official files:
composer domains:audit -- --annex-vi=/path/to/VI.xlsx --annex-vii=/path/to/VII.xlsx --report=/tmp/nt009-report.json
```

The audit first validates both workbook bytes against SHA-256 values independently
observed on **2026-10-05**, not government-issued signatures. It then reads
the layout worksheet rows with literal field names (ignoring, not evaluating,
spreadsheet helper formulas) and compares the complete
indicator-code/description/location triplets with the existing Anexo C v1.01
snapshot. Both known official indicator worksheet names (`INDOP` and `cIndOp Public`) are
supported; unknown sheet names fail explicitly. The NT009 sheet identifies A as the
six-digit indicator, C as the supply characteristic and D as the location in the DFe.
Its column J is a separate NFS-e IBS-incidence classification and must not be
substituted for the official DFe location text. The JSON report lists additions, removals and text changes, as well
as original cell coordinates for relevant NT009 layout entries.

**Interpretation boundary:** exported raw layout rows do *not* automatically
encode complex field conditions, business-rule activation or authorization by
the SEFIN API. The output is audit evidence, not a replacement XSD or a
compliance declaration. Neither Anexo VIII consultative correlations nor new
NT009 fields are activated in production from this tool. The existing
`DpsData`, `XmlBuilder`, `OfficialDomainCatalog` and XSD validators remain
unchanged until a separately reviewed implementation validates the full
normative conditions and deployment dates.

If official sources cannot be downloaded, the auxiliary evidence GitHub
workflow may be inconclusive; the offline PHPUnit suite remains deterministic.

## Frozen Annex VII versioned snapshot

`resources/domains/indicadores-operacao-ibscbs-v1.03.00.tsv` records **all 40**
rows read directly from the official `cIndOp Public` sheet (A/C/D) on
2026-10-08, sourced from the immutable XLSX with SHA-256
`95f30a44ee94adeea57fb92f3a0337b1e62fdc106f393e73894087889be9e5aa`.
The auxiliary CI audit compares all 40 triplets byte-for-byte against the
live official workbook. It is a **separate** snapshot: the existing production
`OfficialDomainCatalog::operationIndicator()` deliberately continues to read
Anexo C v1.01 until the new fiscal contract's effective date and schema are
confirmed. The Annex VII table contains codes applicable to multiple fiscal
document types; no unverified `indNFSe` restrictions have been added.
42 changes: 42 additions & 0 deletions resources/domains/indicadores-operacao-ibscbs-v1.03.00.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Indicadores de operação IBS/CBS — ANEXO_VII v1.03.00 (NT009 v1.01)
# Formato: cIndOp<TAB>característica do fornecimento<TAB>local a identificar no DF-e
010101 Presencial, com retirada no estabelecimento do fornecedor Estabelecimento do fornecedor como o local da entrega ou disponibilização do bem ao destinatário
010102 Presencial, com retirada fora do estabelecimento do fornecedor Local da realização da operação e sua retirada
010103 Não presencial, com entrega ou disponibilização em endereço fornecido Endereço do destinatário fornecido para entrega ou disponibilização (assim considerado o destino final indicado pelo adquirente)
010104 Licitação promovida pelo poder público de bem apreendido ou abandonado ou leilão judicial Local onde se encontra o bem móvel material
010105 Constatação de irregularidade pela falta de documentação fiscal ou pelo acobertamento por documentação inidôneo Local onde se encontra o bem móvel material (Local da retirada)
010106 Locação de bem móvel material em aquisições realizadas de forma centralizada por contribuinte sujeito ao regime regular do IBS e da CBS que possui mais de um estabelecimento e que não estejam sujeitas a vedação à apropriação de créditos Local do domicílio principal do adquirente (estabelecimento matriz)
010201 Entrega ou disponibilização do bem Domicílio principal do destinatário
010202 Entrega ou disponibilização do bem Domicílio principal do destinatário
020101 Realização de operações com bem imóvel, bem imaterial, inclusive direito, relacionado a bem imóvel Localidade do imóvel
020102 Realização de operações com bem imóvel, bem imaterial, inclusive direito, relacionado a bem imóvel de Características Especiais (BICE) Localidade do imóvel (BICE)
020201 Execução de serviços diversos prestados fisicamente sobre bem imóvel Localidade do imóvel
020202 Execução de serviços sobre Bens Imóveis de Características Especiais (BICE) Localidade do imóvel (BICE)
020301 Execução dos serviços de administração e intermediação de bens imóveis Localidade do imóvel
020401 Execução de serviços sobre Bens Imóveis de Características Especiais (BICE) Localidade do imóvel (BICE)
030101 Execução de serviços diversos exclusivamente prestados fisicamente sobre a pessoa ou integralmente fruídos presencialmente por pessoa física Estabelecimento do fornecedor como local da prestação
030102 Execução de serviços diversos exclusivamente prestados fisicamente sobre a pessoa ou integralmente fruídos presencialmente por pessoa física Local da prestação do serviço diverso do estabelecimento do fornecedor
040101 Execução de serviços de planejamento, organização e administração de feiras, exposições, congressos, espetáculos, exibições e congêneres Local do evento a que se refere o serviço
050101 Execução de serviços diversos prestados fisicamente sobre bem móvel material Estabelecimento do fornecedor como local da prestação
050102 Execução de serviços diversos prestados fisicamente sobre bem móvel material Local da prestação do serviço diverso do estabelecimento do fornecedor
050201 Execução de serviços portuários Local do porto como local da prestação
060101 Execução de serviços de transporte de passageiros Local de início do transporte
070101 Execução de serviço de transporte de carga Endereço fornecido para entrega do bem
070102 Execução de serviço de transporte de carga Local da retirada do bem
080101 Execução de serviços de exploração de via Local da prestação correspondente à extensão da via explorada, proporcionalmente ao território dos entes tributantes
090101 Execução de serviços de telefonia fixa e demais serviços de comunicação prestados por meio de cabos, fios, fibras e meios similares Local de instalação do terminal
090102 Execução de serviços de telefonia fixa e demais serviços de comunicação prestados por meio de cabos, fios, fibras e meios similares Local do domicílio principal do adquirente (assim considerada a matriz), nas aquisições realizadas de forma centralizada por contribuinte sujeito ao regime regular do IBS e da CBS que possui mais de um estabelecimento e que não estejam sujeitas a vedação à apropriação de créditos
100101 Realização de operações de cessão de espaço para prestação de serviços publicitários Local do domicílio principal do adquirente residente ou domiciliado no País. Nas aquisições indicadas no art. 11, §4º, II, considera-se domicílio principal do adquirente o estabelecimento matriz
100102 Realização de operações de cessão de espaço para prestação de serviços publicitários Local do domicílio principal do destinatário residente ou domiciliado no País, caso o adquirente não seja residente ou domiciliado no País
100201 Realização de operações de cessão de espaço para prestação de serviços publicitários local do domicílio principal do destinatário residente ou domiciliado no País
100301 Execução dos demais serviços em operações não especificadas em outros indicadores ou, nos serviços de que trata o inc. III do art. 11, esses sejam, ainda que parcialmente, prestados à distância Local do domicílio principal do adquirente residente ou domiciliado no País. Nas aquisições indicadas no art. 11, §4º, II, considera-se domicílio principal do adquirente o estabelecimento matriz
100302 Execução dos demais serviços em operações não especificadas em outros indicadores ou, nos serviços de que trata o inc. III do art. 11, esses sejam, ainda que parcialmente, prestados à distância Local do domicílio principal do destinatário residente ou domiciliado no País, caso o adquirente não seja residente ou domiciliado no País
100401 Execução dos demais serviços em operações não especificadas em outros indicadores ou, nos serviços de que tratam o inc. III, estes sejam, ainda que parcialmente, prestados à distância local do domicílio principal do destinatário residente ou domiciliado no País
100501 Realização de demais operações com bens não especificadas em outros indicadores Local do domicílio principal do adquirente residente ou domiciliado no País. Nas aquisições indicadas no art. 11, §4º, II, considera-se domicílio principal do adquirente o estabelecimento matriz
100502 Realização de demais operações com bens não especificadas em outros indicadores Local do domicílio principal do destinatário residente ou domiciliado no País, caso o adquirente não seja residente ou domiciliado no País
100601 Realização de demais operações com bens não especificadas em outros indicadores local do domicílio principal do destinatário residente ou domiciliado no País
110101 Operações destinadas a consumo Local da entrega ou disponibilização
110201 Operações que não envolvam efetivo consumo: a) no fornecimento de serviços de transmissão de energia elétrica; e b) nas demais operações, inclusive nas hipóteses de geração, distribuição ou comercialização de energia elétrica. Local do estabelecimento principal do adquirente (assim considerada a matriz), conforme §4º do art. 11
120101 Disponibilização de energia elétrica Local do estabelecimento do agente ou de seus representados que figurem na posição devedora da liquidação financeira apurada pela Câmara de Comercialização de Energia Elétrica
130101 Execução do transporte dutoviário de gás natural, na contratação de capacidade de entrada de gás natural do duto, nos termos da legislação aplicável Local do estabelecimento principal do fornecedor
130201 Execução do transporte dutoviário de gás natural, na contratação de capacidade de saída do gás natural do duto Local do estabelecimento principal do adquirente
63 changes: 62 additions & 1 deletion tools/bin/domains.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use LibreCodeCoop\NfsePHP\Tools\AnnexReader;
use LibreCodeCoop\NfsePHP\Tools\DomainTableGenerator;
use LibreCodeCoop\NfsePHP\Tools\Nt009Inspector;
use LibreCodeCoop\NfsePHP\Tools\PortalIndexDiscovery;
use LibreCodeCoop\NfsePHP\Tools\SourceVerifier;

Expand Down Expand Up @@ -69,6 +70,38 @@
}
echo "Verified " . count($outputs) . " deterministic snapshots\n";
} elseif ($command === 'audit') {
if (isset($options['download-dir'])) {
$destination = $options['download-dir'];
if (!is_dir($destination) && !mkdir($destination, 0o700, true)) {
throw new RuntimeException('Cannot create official document download directory');
}
$manifest = json_decode(
(string) file_get_contents($root . '/resources/domains/sources.json'),
true,
512,
JSON_THROW_ON_ERROR
);
foreach (['annex-vi-nt009' => 'annex-vi', 'annex-vii-nt009' => 'annex-vii'] as $id => $name) {
$source = null;
foreach ($manifest['sources'] as $entry) {
if ($entry['id'] === $id) {
$source = $entry;
break;
}
}
if ($source === null || !str_starts_with(
$source['url'],
'https://www.gov.br/nfse/'
)) {
throw new RuntimeException('Missing or non-official source: ' . $id);
}
$file = $destination . '/' . $id . '.xlsx';
if (file_put_contents($file, SourceVerifier::fetchOfficial($source['url'])) === false) {
throw new RuntimeException('Failed to save official source: ' . $id);
}
$options[$name] = $file;
}
}
foreach (['annex-vi', 'annex-vii'] as $name) {
if (!isset($options[$name])) {
throw new InvalidArgumentException("Missing --{$name}=PATH");
Expand All @@ -85,6 +118,34 @@
echo "{$name}: {$hash}; sheets: " . implode(', ', $reader->sheetNames()) . "\n";
$reader->close();
}
$comparison = (new Nt009Inspector())->inspect(
$options['annex-vi'],
$options['annex-vii'],
$root . '/resources/domains/indicadores-operacao-ibscbs-v1.01.tsv'
);
$json = json_encode($comparison, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
| JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
if (isset($options['report'])) {
if (file_put_contents($options['report'], $json) === false) {
throw new RuntimeException('Unable to save NT009 comparison');
}
}
echo 'NT009 Anexo VII: ' . $comparison['annex_vii_count']
. ' indicators; added=' . count($comparison['added_codes'])
. ', changed=' . count($comparison['changed_codes'])
. ', removed=' . count($comparison['removed_codes']) . "\n";
if (getenv('GITHUB_STEP_SUMMARY') !== false) {
file_put_contents(
(string) getenv('GITHUB_STEP_SUMMARY'),
"## NT009 source comparison\n\n" . 'Annex VII: '
. $comparison['annex_vii_count'] . ' indicators, added: '
. count($comparison['added_codes']) . ', changed: '
. count($comparison['changed_codes']) . ', removed: '
. count($comparison['removed_codes'])
. "\n\nA published annex does not establish production applicability.\n",
FILE_APPEND
);
}
} elseif ($command === 'watch') {
$manifest = $options['manifest'] ?? $root . '/resources/domains/sources.json';
$verifier = new SourceVerifier();
Expand Down Expand Up @@ -133,7 +194,7 @@
throw new InvalidArgumentException(
"Usage: php tools/bin/domains.php {generate|check|audit|watch} --name=PATH\n"
. "generate/check: --annex-a= --annex-b= --annex-c= --output= [--annex-vii= --expected-vii=N]\n"
. "audit: --annex-vi= --annex-vii=\nwatch: [--manifest= --report=]\n",
. "audit: --annex-vi= --annex-vii= [--report=] OR --download-dir= [--report=]\nwatch: [--manifest= --report=]\n",
);
}
} catch (Throwable $error) {
Expand Down
6 changes: 5 additions & 1 deletion tools/src/AnnexReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function sheetNames(): array
/**
* @return list<array<string, string>>
*/
public function rows(string $sheetName): array
public function rows(string $sheetName, bool $skipFormulaCells = false): array
{
$sheet = null;
foreach ($this->workbook->getWorksheetIterator() as $candidate) {
Expand All @@ -66,6 +66,10 @@ public function rows(string $sheetName): array
$name = Coordinate::stringFromColumnIndex($i);
$cell = $sheet->getCell($name . $r);
if ($cell->getDataType() === DataType::TYPE_FORMULA) {
if ($skipFormulaCells) {
// Spreadsheet helper formula: do not execute or trust its cached result.
continue;
}
throw new \UnexpectedValueException("Formula not allowed in {$sheetName}!{$name}{$r}");
}
$raw = $cell->getFormattedValue();
Expand Down
Loading
Loading