# Decisões de arquitetura

Copyright (c) 2026 RAVAPI Soluções. www.ravapi.com

Cada entrada registra o que foi decidido, por quê, e o que custaria reverter.
Serve para quem chegar depois não refazer a discussão — nem desfazer sem saber
o preço.

---

## 1. Recibo não é documento fiscal

Prova de quitação civil (Código Civil, arts. 319 e 320). Não exige certificado
digital, SEFAZ nem homologação municipal, e **não substitui NF-e ou NFS-e**.

Como não há autoridade externa validando, a credibilidade do documento vem
inteiramente da disciplina interna de numeração, imutabilidade e
rastreabilidade. É o diferencial do produto, não um detalhe técnico.

**Reversão:** não se aplica. É a natureza do documento.

---

## 2. Direção é atributo da série, não do documento

Recebimento e pagamento são conferidos de formas diferentes pela contabilidade.
Misturar os dois no mesmo sequencial atrapalha a conferência do talão.

A direção é escolhida ao criar a série e fica imutável a partir do primeiro
documento emitido — antes disso é erro de cadastro e pode ser corrigido.

**Reversão:** impossível depois que a série emitiu. A saída é desativar a série
e criar outra.

---

## 3. Numeração sem `SEQUENCE`

Sequence não faz rollback: uma transação abortada consumiria o número e abriria
buraco no talão. O contador vive em `series.proximo_numero`, travado com
`SELECT … FOR UPDATE`, e um trigger impede que retroceda.

**Custo:** emissões concorrentes na mesma série serializam. Irrelevante no
volume de um restaurante; se aparecer cliente de volume alto, aí se discute.

---

## 4. A aplicação não tem `INSERT` em `receipts`

A única porta de entrada de documento selado é `recibos.selar_recibo()`, que
roda como `SECURITY DEFINER`. Não existe caminho no código que crie recibo
pulando a numeração ou o encadeamento — nem por bug, nem por injeção de SQL.

**Reversão:** conceder o `INSERT` desmontaria a garantia inteira. Não fazer.

---

## 5. Cadeia de hash no escopo da série

Cada talão tem sua própria linha do tempo, começando no próprio bloco gênese —
como a contabilidade já confere talão. Numeração e cadeia com o mesmo escopo
significam um único lock governando as duas coisas, e séries diferentes emitem
em paralelo.

Decidido antes de existir documento em produção, que era a janela barata.

**Consequências:** `verify-chain` percorre N cadeias em vez de uma, e
`verificarCadeia()` no TypeScript só aceita documentos de uma série por vez —
misturar séries acusa falso positivo.

**Reversão:** migration em tabela imutável. Cara.

---

## 6. Payload canonicalizado na aplicação, hash calculado no banco

A canonicalização precisa ser determinística e testável, e isso se faz melhor
em TypeScript, com vetor de regressão congelado. Mas o SHA-256 é calculado
dentro da função de selo, com o lock na mão — assim ninguém vence a corrida
pelo `hash_anterior`.

`convert_to(texto, 'UTF8')`, **nunca** `::bytea`: o cast não converte texto em
bytes, interpreta a string como sintaxe de escape. Ver CHANGELOG 0.3.0.

---

## 7. Cancelamento é contra-registro

Cancelar nunca apaga nem altera o recibo: insere linha em
`receipt_cancellations` com motivo obrigatório. O documento permanece, ganha
tarja na impressão, e **o número fica consumido para sempre**.

---

## 8. Segregação de funções: quem emite não cancela

Cancelar é a única escrita que existe sobre documento selado. Concentrar
emissão e cancelamento na mesma pessoa é o primeiro ponto que um contador
levanta. Cancelamento é privativo de supervisor e proprietário.

A permissão de emissão é **por série**: quem lança entrada não precisa ser quem
lança saída.

---

## 9. Licença nunca bloqueia leitura

Bloquear a emissão de quem está em atraso já impede o cliente de cumprir
obrigação com terceiros — é duro, mas é a alavanca comercial. Bloquear a
**leitura** seria reter dado que é dele.

Com a licença suspensa continuam liberados: ler, buscar, reimprimir, gerar PDF,
enviar por e-mail, verificação pública, exportação completa e **cancelamento**
— cancelar é correção de erro passado, e impedir isso prenderia o cliente a um
documento errado por questão comercial.

Só param: selar recibo novo e criar empresa, série ou usuário.

**Isso precisa estar no contrato do Klavo, não só no código.**

---

## 10. Licença verificada offline

Token assinado em Ed25519, validado localmente com a chave pública. O produto
não tem a chave privada e não emite licença. Heartbeat diário busca renovação,
mas falha de rede não interrompe nada.

O Klavo sair do ar não pode derrubar a emissão de recibos de ninguém.

---

## 11. Um só entitlement no modo embarcado

Quando o gerador roda dentro do sistema de freelancers, não existe licença
separada: a mesma licença Klavo carrega `produtos.recibos` com
`"modo": "embarcado"`. Dois contratos pelo mesmo cliente seria erro comercial
e confusão de suporte.

---

## 12. Impressão sai do snapshot, nunca do cadastro atual

O recibo grava cópia congelada de emitente, credor e devedor. Se a empresa
mudar de endereço ou a contraparte de razão social, os documentos já emitidos
continuam saindo como saíram no dia.

---

## 13. O papel de conexão não pode ser superusuário

`FORCE ROW LEVEL SECURITY` alcança o dono das tabelas, mas superusuário ignora
RLS por definição do PostgreSQL. Conectando como superusuário, o isolamento
entre tenants deixa de existir — silenciosamente.

Usar `recibos_app`, e só ele.

---

## 14. `app.tenant_id` em toda transação, com falha explícita

Sem o tenant definido, as políticas não casam com nada e a consulta volta
vazia. É falha fechada, que é o comportamento seguro, mas silenciosa: pareceria
"não tem dados" em vez de erro. Por isso o contexto estoura antes de tocar no
banco.

---

## 16. Decisões comerciais e de produto — 14/09/2026

Fechadas com o Rafael. As que viraram trabalho estão marcadas.

| # | Decisão | Efeito |
|---|---|---|
| Verificação pública | `ravapi.com/v/{código}` | já impresso nas amostras; recibo em papel não se atualiza, então o endereço está congelado |
| Descrição | limite de 300 caracteres na emissão | feito na migration 005 |
| Storage de assets | S3-compatible | **a fazer:** implementar o adapter; o modo embarcado pode rodar em outro servidor |
| Preço | por empresa, com pacote de recibos | planos concretos ficam para depois; `limites.empresas` e `limites.recibos_mes` já suportam |
| Trial | 15 dias | trava `em_trial` feita na 005; emissão da licença é do Klavo (ver 16.1) |
| Contrato | próprio, não aditivo ao existente | a escada de degradação precisa estar escrita nele, não só no código |
| Tolerância | 15 dias após a primeira licença paga | errar para mais custa uma semana de inadimplente emitindo; errar para menos trava adimplente por problema de rede |
| Papéis | os quatro bastam | contador multiempresa usa um vínculo por empresa |
| RPA | fora do escopo — pagamentos esporádicos (ver 16.2) | colunas seguem reservadas |
| Cadastro rápido | dentro do rascunho, CPF como chave de busca | feito na migration 005 (ver 16.3) |

### 16.1 Tolerância é zero durante o trial

Trial de 15 dias somado a tolerância de 15 dias daria 30 dias grátis. A
tolerância existe para proteger cliente adimplente de problema de rede ou
boleto atrasado — aplicada a um trial vencido, vira extensão automática do
trial. O Klavo emite `grace_dias = 0` enquanto a licença for de trial.

### 16.2 RPA fora do escopo

Decisão do Rafael: a plataforma trata pagamentos esporádicos, a pessoas
variadas e a fornecedores pessoa física. Não faz RPA e não calcula retenção.

O caráter esporádico afasta o risco maior, que é **vínculo empregatício** —
habitualidade é um dos elementos que o caracterizam, junto com subordinação,
pessoalidade e onerosidade. Pagamento eventual a pessoas diferentes fica longe
disso.

O que a frequência não decide é a retenção. A obrigação de reter INSS e IRRF
quando uma PJ paga pessoa física por **serviço prestado** é por operação, não
por habitualidade: um serviço único de autônomo já atrai retenção. O eixo real
é serviço × mercadoria — fornecedor PF que vende produto não gera retenção de
INSS; prestador PF que executa serviço gera, mesmo uma vez só.

Confirmar com o contador do cliente antes do primeiro uso em produção. Isto
aqui é leitura de arquiteto, não de contador.

**Aviso de recorrência: descartado.** A ideia inicial era alertar quando a mesma
pessoa física recebesse mais de três vezes em noventa dias. Ela mirava
habitualidade — justamente o que não ocorre nesta operação — e ficaria silenciosa
no caso que importa, o pagamento único por serviço. Aviso que dispara na hora
errada é pior que nenhum: ensina o usuário a ignorar avisos.

Fica o que já existe: o rodapé impresso dizendo que o recibo comprova quitação e
não substitui documento fiscal, e as colunas de retenção reservadas em
`receipts` caso o cenário mude.

### 16.3 CPF como chave de busca exige unicidade

`counterparties` não tinha restrição em `(tenant_id, cpf_cnpj)`. Sem ela,
localizar pelo CPF pode devolver duas pessoas e o cadastro rápido perde o
sentido. O cadastro rápido passa a buscar antes de inserir: documento já
existente traz a pessoa, não duplica.

Migration nova, em tabela mutável — barato agora, caro depois que houver base.

---

## 17. Empresa muda o contrato, série não — 15/09/2026

Decisão do Rafael. Criar série é de graça e sem limite — sempre foi assim,
nunca teve `LimiteDoPlano` nenhum em cima. Criar empresa é diferente: passar
do número contratado em `limites.empresas` não bloqueia a criação, mas muda o
que o cliente paga — a cobrança é por medição, proporcional, do lado do
Klavo, e o cliente precisa ser avisado quando isso acontece.

**O que isto implica, e o que já está feito:**

- `CadastrosService.criarEmpresa()` nunca lança `LimiteDoPlano`. Sempre cria,
  e devolve `{ empresa, acimaDoPlano, contratadas, totalAtivas }` — o dado
  cru pra quem chamar decidir o que fazer com a informação.
- Reportar `acimaDoPlano` pro Klavo (pra cobrança acontecer) e notificar o
  cliente são coisas que **este repositório não faz** — bate com a divisão
  de responsabilidade já registrada (§9.1 do escopo: "o gerador de recibos
  apenas valida e reporta uso"). Reportar é fase 7 (Klavo); notificar por
  e-mail é fase 8. Nenhuma das duas existe ainda.
- Provado, não afirmado: `criarEmpresa()` tem teste mostrando que uma
  empresa acima do limite é criada mesmo assim, com a sinalização certa —
  não que a cobrança ou o aviso aconteçam, porque isso ainda não existe.

## 18. Login único da RAVAPI — desenhado agora, construído depois — 15/09/2026

Não existe hoje um serviço de login único da RAVAPI — nenhuma API, nenhum
protocolo, nenhuma documentação em lugar nenhum. A decisão do Rafael não foi
"integrar com X", foi "deixar a estrutura pronta pra caber uma integração
futura, seja ela qual for".

A abertura pra isso já existia antes desta decisão: `IdentityProvider`
(`src/core/identity.ts`) é a única porta de entrada de identidade que o
núcleo conhece — `usuarioAtual()`, `listarMembros()`, `administraUsuarios()`.
Nada no domínio ou nos controllers depende de como esses três métodos são
implementados. Login único da RAVAPI, quando existir, entra como uma terceira
implementação dessa interface — ao lado de `IdentidadeLocal` (senha própria,
modo SaaS) e `IdentidadeDelegada` (modo embarcado) — sem tocar em
`RecibosService`, `CadastrosService` nem nos controllers.

O que foi construído agora (login local, e-mail e senha) segue essa mesma
porta — é a implementação que existe hoje, não uma prévia do login único.
Trocar uma pela outra depois é troca de provider, não reescrita.

---

## Pendências

Tudo o que estava aqui foi decidido em 16, em 14/09/2026. O que resta é
execução e uma conferência manual.

**Código**

1. ~~Validação de 300 caracteres na descrição~~ — feito na 005.
2. ~~Restrição única em `(tenant_id, cpf_cnpj)` e cadastro rápido~~ — feito na 005.
3. Adapter de storage S3-compatible — pré-requisito da assinatura em tela.
4. Fase 5: página pública de verificação e captura de assinatura.

**Fora deste repositório**

5. `grace_dias = 0` durante o trial — emissão da licença, lado do Klavo.
   A trava do lado do produto (`em_trial`) já está feita na 005: mesmo que a
   licença venha com tolerância, ela é ignorada durante o trial.
6. Planos concretos de preço — conversa comercial.
7. Redação do contrato próprio, com a escada de degradação escrita nele.

**Manual, bloqueante**

8. Imprimir as amostras de `amostras/` em A4 com "tamanho real" e conferir se o
   corte cai na metade da folha e se sobra espaço para assinar à mão. É o
   critério de aceite da Fase 4 e não é automatizável. Se estiver fora, o
   template muda e tudo construído sobre ele muda junto.
