Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7e67ad2
feat: add typed NT009 draft data for recipients, payments and adjustm…
vitormattos Oct 9, 2026
92288a9
feat: expose opt-in structured NT009 fields in preview input
vitormattos Oct 9, 2026
b5c7005
fix: reject deprecated deduction group in NT009 structural preview
vitormattos Oct 9, 2026
125becf
feat: serialize NT009 vAjusteBC modes without legacy vDedRed
vitormattos Oct 9, 2026
0ccb073
feat: validate bounded NT009 payment linkage records and uniqueness
vitormattos Oct 9, 2026
9ce5efd
feat: validate and serialize bounded NT009 movable-rental records
vitormattos Oct 9, 2026
9939e1e
feat: add explicit and escaped NT009 recipient identity and addresses
vitormattos Oct 9, 2026
303172c
feat: wire structured NT009 preview groups in official element order
vitormattos Oct 9, 2026
9e1d904
test: cover NT009 group ordering, escaping, invalid input and legacy …
vitormattos Oct 9, 2026
58eeaf6
style: format NT009 preview regression tests
vitormattos Oct 9, 2026
5cb6a86
docs: identify implemented draft groups and remaining NT009 productio…
vitormattos Oct 9, 2026
902101a
feat: model optional NT009 real estate and lease information explicitly
vitormattos Oct 9, 2026
5785bd6
feat: serialize bounded NT009 property units and lease groups for pre…
vitormattos Oct 9, 2026
f4aa160
feat: model NT009 condominium charges, details and discounts
vitormattos Oct 9, 2026
d357c53
feat: bound and reconcile condominium preview charge groups
vitormattos Oct 9, 2026
d0b049c
feat: expose NT009 property and condominium groups in opt-in preview
vitormattos Oct 9, 2026
5927c83
test: validate NT009 real estate and condominium preview structures
vitormattos Oct 9, 2026
177163b
docs: record draft-only real estate and condominium contracts and lim…
vitormattos Oct 9, 2026
540e0dd
feat: model three mutually exclusive NT009 adjustment document refere…
vitormattos Oct 9, 2026
a794a16
feat: add document-based adjustment mode and reuse vetted identity se…
vitormattos Oct 9, 2026
1dcf711
feat: serialize NT009 adjustment documents with typed reference choices
vitormattos Oct 9, 2026
aa376a5
feat: implement mutually exclusive document-backed NT009 adjustment mode
vitormattos Oct 9, 2026
0c3f37d
test: cover all NT009 document-backed adjustment choices and invalid …
vitormattos Oct 9, 2026
5b10c06
docs: describe explicit NT009 document adjustment alternatives and re…
vitormattos Oct 9, 2026
63d03bd
fix: preserve distinct Annex VI supplier address XML hierarchy
vitormattos Oct 9, 2026
fb38eca
test: verify distinct Annex VI supplier address nesting and XML escaping
vitormattos Oct 9, 2026
b08e5ce
fix: reconcile large condominium amounts without integer overflow or …
vitormattos Oct 9, 2026
eb14419
test: prove 15-digit condominium amounts reconcile without overflow
vitormattos Oct 9, 2026
d0d1dd8
fix: resolve preview XPath regression, Psalm union branch and style
vitormattos Oct 9, 2026
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
37 changes: 34 additions & 3 deletions docs/NT009-CONTRACT-IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,40 @@ verified `ind_gIBSCBS` flag is false, and required in the appropriate
conditional form if it is true.

**The preview deliberately cannot model the complete 450-row new contract.**
In particular the new `vAjusteBC` structure, detailed `dest` group,
`gPgtoVinc`, `imovel`, `bensMoveis`, `condominios` and other conditional
groups are not implemented for production serialization. The builder rejects
It now includes explicitly typed, separately validated structural drafts for:

- `infDPS/dest` (Annex VI rows 180-200): one identity, optional national or
foreign address; identifiers and textual values are validated before
insertion, with DOM text nodes used for safe XML escaping.
- `infDPS/valores/vAjusteBC` (rows 293-336): exactly one ISSQN
percentage, monetary or document-backed mode. The document mode accepts
up to 1,000 typed `docAjusteBC` entries with a choice of
`dFeNacional`, `docFiscalOutro` or `docOutro`, optional dates and
explicit `fornec` identity. An optional externally justified
`vAjusteBCIBSCBSComExt` may be provided; fiscal applicability is not
inferred by the SDK.
- `IBSCBS/bensMoveis` (rows 404-407): up to 1,000 records, restricted to
`cTribNac=99.04.01`, preserving the 8-digit NCM and bounded quantities.
- `IBSCBS/gPgtoVinc/pgto` (rows 443-449): up to 99 records, with distinct
transaction IDs and payment numbers and the published payment method codes.
- `IBSCBS/imovel` (rows 383-403): explicit municipality, the
classification-restricted optional `gLocacao`, and up to 99 units with
bounded address and per-unit adjustment data.
- `IBSCBS/condominios` (rows 408-421): source-coded charge categories and
optional detail and discount records, with exact-cent reconciliation of
`gDetCobranca` to the charge and total charge values to `vServ`.

The builder now rejects legacy `DpsData::deducaoReducao` in NT009 preview
instead of silently copying the obsolete `vDedRed` group. This prevents
mistaking old deduction rules for the new, not-yet-effective schema.
No taxpayer identity or payment values are inferred.

Still missing are independently confirmed adjustment-type domains
and tax-code repercussions, externally verified tax-code correlations,
specialized conditional groups not yet modeled, and — most importantly —
validation against a published,
environment-specific **effective NT009 XSD**. None of these review-only
structures is enabled in the real emission path. The builder rejects
mixing legacy IBS fields and the new preview options. It removes the obsolete
`xsi:schemaLocation` reference, without claiming that it matches a new
schema. The existing `DpsSchemaValidator` remains bound to the February
Expand Down
30 changes: 30 additions & 0 deletions src/Dto/Nt009AdjustmentDocumentData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/**
* One document-backed vAjusteBC entry (Annex VI rows 297-335).
* The reference is a discriminated PHP union; no arbitrary XML is accepted.
*/
final readonly class Nt009AdjustmentDocumentData
{
public function __construct(
/** tpAjusteBC: numeric official type (domain-specific applicability deferred). */
public string $tipo,
/** vTotDoc, represented as decimal string to avoid float calculations. */
public string $valorTotalDocumento,
/** vAjusteAplic, represented as decimal string. */
public string $valorAjustado,
public Nt009NationalInvoiceReference|Nt009OtherFiscalReference|Nt009OtherDocumentReference $referencia,
public ?string $descricaoTipo = null,
public ?string $dataEmissao = null,
public ?string $dataCompetencia = null,
public ?Nt009RecipientData $fornecedor = null,
) {
}
}
30 changes: 30 additions & 0 deletions src/Dto/Nt009BaseAdjustmentData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/**
* Review-only NT009 valores/vAjusteBC. Monetary, percentage and explicit
* document-backed alternatives are mutually exclusive and never inferred.
*/
final readonly class Nt009BaseAdjustmentData
{
/**
* @param list<Nt009AdjustmentDocumentData> $documentos
*/
public function __construct(
/** pAjusteBCISSQN, mutually exclusive with valorIssqn. */
public ?string $percentualIssqn = null,
/** vAjusteBCISSQN, mutually exclusive with percentualIssqn. */
public ?string $valorIssqn = null,
/** vAjusteBCIBSCBSComExt, only when separately applicable. */
public ?string $valorIbsCbsComExterior = null,
/** Document-based alternative to percentage or monetary adjustment. */
public array $documentos = [],
) {
}
}
23 changes: 23 additions & 0 deletions src/Dto/Nt009CondominiumChargeData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Draft-only gCobranca for a condominium, Annex VI rows 410-417. */
final readonly class Nt009CondominiumChargeData
{
/**
* @param list<Nt009CondominiumDetailData> $detalhes
*/
public function __construct(
public string $tipo,
public string $valor,
public ?string $descricaoTipo = null,
public array $detalhes = [],
) {
}
}
23 changes: 23 additions & 0 deletions src/Dto/Nt009CondominiumData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Draft-only condominium group; source Annex VI 408-421. */
final readonly class Nt009CondominiumData
{
/**
* @param list<Nt009CondominiumChargeData> $cobrancas
* @param list<Nt009CondominiumDiscountData> $descontos
*/
public function __construct(
public string $vencimentoOriginal,
public array $cobrancas,
public array $descontos = [],
) {
}
}
19 changes: 19 additions & 0 deletions src/Dto/Nt009CondominiumDetailData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Draft-only optional gDetCobranca breakdown. */
final readonly class Nt009CondominiumDetailData
{
public function __construct(
public string $tipo,
public string $valor,
public ?string $descricaoTipo = null,
) {
}
}
19 changes: 19 additions & 0 deletions src/Dto/Nt009CondominiumDiscountData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Draft-only gDesconto for a condominium. */
final readonly class Nt009CondominiumDiscountData
{
public function __construct(
public string $tipo,
public string $valor,
public string $descricaoTipo,
) {
}
}
16 changes: 16 additions & 0 deletions src/Dto/Nt009DpsPreviewData.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
*/
final readonly class Nt009DpsPreviewData
{
/**
* @param list<Nt009LinkedPaymentData> $pagamentosVinculados
* @param list<Nt009MovableAssetData> $bensMoveis
*/
public function __construct(
/** finNFSe: 0 regular, 1 credit, 2 debit. */
public int $finalidade,
Expand Down Expand Up @@ -43,6 +47,18 @@ public function __construct(
public ?string $valorAjusteCbs = null,
/** regApIBSCBSSN 1, 2 or 3, when applicable. */
public ?int $regimeApuracaoSimples = null,
/** Recipient identity is required when indDest=1. */
public ?Nt009RecipientData $destinatario = null,
/** Explicit new base-adjustment form; never repurpose legacy vDedRed. */
public ?Nt009BaseAdjustmentData $ajusteBase = null,
/** Up to 1000 movable-rental items for cTribNac 99.04.01. */
public array $bensMoveis = [],
/** Up to 99 transaction records under IBSCBS/gPgtoVinc. */
public array $pagamentosVinculados = [],
/** Optional property operation details, Annex VI rows 383-403. */
public ?Nt009RealEstateData $imovel = null,
/** Optional condominium billing, Annex VI rows 408-421. */
public ?Nt009CondominiumData $condominios = null,
) {
}
}
21 changes: 21 additions & 0 deletions src/Dto/Nt009LeaseData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Draft-only property lease group gLocacao. */
final readonly class Nt009LeaseData
{
public function __construct(
public string $percentualCopropriedade,
public string $valorTotalOperacao,
public ?string $descontoIncondicional = null,
public ?string $descontoCondicional = null,
public ?string $dataVencimentoOriginal = null,
) {
}
}
28 changes: 28 additions & 0 deletions src/Dto/Nt009LinkedPaymentData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/**
* One entry of IBSCBS/gPgtoVinc/pgto, as published in Annex VI v1.04.01.
*/
final readonly class Nt009LinkedPaymentData
{
public function __construct(
/** nPag: unique 1-3 digit payment number. */
public string $numeroPagamento,
/** idTransacao: unique 2-35 character financial transaction identifier. */
public string $identificadorTransacao,
/** tpMeioPgto: published means of payment, e.g. 17 (dynamic PIX). */
public string $tipoMeioPagamento,
/** CNPJReceb: 14-character recipient CNPJ, potentially alphanumeric. */
public string $cnpjRecebedor,
/** CNPJBasePSP: 8-character base of the payment service provider CNPJ. */
public string $cnpjBasePsp,
) {
}
}
24 changes: 24 additions & 0 deletions src/Dto/Nt009MovableAssetData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/**
* One rental movable asset: IBSCBS/bensMoveis, only for cTribNac 99.04.01.
*/
final readonly class Nt009MovableAssetData
{
public function __construct(
/** cNCMBemMovel: NCM with exactly 8 digits. */
public string $ncm,
/** xNCMBemMovel: human-readable name (1-150 chars). */
public string $descricao,
/** qtdNCMBemMovel: positive whole quantity, at most three digits. */
public int $quantidade,
) {
}
}
19 changes: 19 additions & 0 deletions src/Dto/Nt009NationalInvoiceReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Choice dFeNacional inside a document-based base adjustment. */
final readonly class Nt009NationalInvoiceReference
{
public function __construct(
public string $tipoChaveDfe,
public string $chaveDfe,
public ?string $descricaoTipoChaveDfe = null,
) {
}
}
18 changes: 18 additions & 0 deletions src/Dto/Nt009OtherDocumentReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Choice docOutro inside a document-based base adjustment. */
final readonly class Nt009OtherDocumentReference
{
public function __construct(
public string $numero,
public string $descricao,
) {
}
}
19 changes: 19 additions & 0 deletions src/Dto/Nt009OtherFiscalReference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Choice docFiscalOutro inside a document-based base adjustment. */
final readonly class Nt009OtherFiscalReference
{
public function __construct(
public string $municipioIbge,
public string $numeroDocumento,
public string $descricaoDocumento,
) {
}
}
19 changes: 19 additions & 0 deletions src/Dto/Nt009PropertyAdjustmentData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreCodeCoop\NfsePHP\Dto;

/** Draft-only gAjusteBCLocImoveis entry for one property unit. */
final readonly class Nt009PropertyAdjustmentData
{
public function __construct(
public string $tipo,
public string $valor,
public ?string $descricaoTipo = null,
) {
}
}
Loading
Loading