Fiscalis — plataforma fiscal multi-empresa

Emissão e apuração fiscal para várias empresas no mesmo sistema, com isolamento de dados por CNPJ. Monólito modular em .NET 9 com oito módulos, Result em vez de exceção e desenvolvimento guiado por especificação.

.NET 9C#PostgreSQLAngular 22EF CoreDocker

O problema

Um sistema fiscal raramente atende uma empresa só. Escritório de contabilidade opera dezenas de clientes; grupo com matriz e filiais precisa que cada CNPJ veja apenas o que é seu. Nesse contexto, vazamento de dado entre empresas não é um defeito de interface — é problema fiscal, com consequência legal.

Esse requisito foi o que decidiu a arquitetura.

Monólito modular, não microsserviços

Oito módulos com fronteira explícita — IdentityAccess, Organizations, Invoicing, TaxReporting, Certificates, FiscalProviders, Notifications, Auditing — cada um com o próprio DbContext. A decisão está registrada em ADR-0001, com o motivo: o custo operacional de rede, observabilidade e transação distribuída não se paga num sistema que ainda não tem escala para justificá-lo, e a fronteira de módulo bem feita permite extrair um serviço depois, se a necessidade aparecer.

Isolamento de tenant

A separação por empresa é feita em camadas, e não numa só:

  1. A aplicação filtra por organização em toda consulta.
  2. A autorização confere o vínculo do usuário com a organização antes de servir.
  3. No banco, cada tabela tem policy de Row Level Security escrita à mão em migração — o Prisma e o EF Core não expressam policy, então isso vive em SQL dentro do histórico de migração, senão vira desvio silencioso.

Sobre a terceira camada, o estado atual, sem maquiagem: as policies existem no banco, mas a conexão da aplicação ainda usa uma role capaz de contorná-las, o que as deixa inertes. Está registrado na spec 012, com as perguntas em aberto e uma ordem de execução em que a troca da conexão só acontece depois de o interceptador existir.

Preferi documentar isso a entregar meia trava. RLS pela metade é pior que RLS nenhuma: dá a sensação de proteção e derruba a aplicação inteira no dia em que a role muda.

Erro esperado não é exceção

Regra de negócio violada devolve Result<T>, não lança (ADR-0003). Nota fiscal recusada pela SEFAZ, certificado expirado e CNPJ inválido são resultados previstos do domínio: modelá-los como exceção esconde o caminho de erro de quem lê o código e transforma controle de fluxo em captura genérica algumas camadas acima.

Valores fiscais no núcleo compartilhado

CNPJ, percentual e base de cálculo são value objects no SharedKernel (ADR-0004), com validação num lugar só. Dois detalhes que essa escolha resolveu: CNPJ alfanumérico, que entra em vigor e quebra validação por dígito (ADR-0007), e numeração de RPS, que tem regra própria por município (ADR-0005).

Um defeito que mostra por que precisão fiscal exige cuidado

O value object de percentual arredondava assim:

FiscalMath.RoundRate(value / 100m) * 100m

A coluna é numeric(7,4) e guarda o percentual na forma humana — 4 casas decimais disponíveis. Dividir por 100 antes de arredondar gastava duas dessas casas no arredondamento e multiplicava o resultado de volta: 5,1234% era gravado como 5,1200%.

Num sistema que apura imposto sobre milhares de notas, erro na quarta casa não fica na quarta casa. Comprovei o comportamento antes de mexer, corrigi para arredondar o valor direto e cobri com 18 testes — o arquivo de teste que a própria documentação do projeto exigia e que não existia.

Qualidade imposta pelo compilador

TreatWarningsAsErrors com analisadores Roslyn e .editorconfig. Aviso não vira dívida técnica porque não compila. Custa disciplina no começo e economiza a discussão sobre "depois a gente limpa".

Desenvolvimento guiado por especificação

Cada bloco de trabalho tem uma spec numerada com critério de aceite, e cada decisão de arquitetura tem um ADR. O que isso muda na prática: quando uma decisão é revista, o registro antigo não é apagado — ele é marcado como substituído, e o novo cita o gatilho. Fica legível por que o sistema é como é.

Stack

.NET 9 e C# no backend, PostgreSQL, Angular 22 no frontend com componentes standalone e sem zone.js, Docker Compose para subir tudo.