Pular para o conteúdo

E-3 — Simulação Econômica

Parte das Colônias de Empresas — Fase 1


Recebe os dados financeiros autodeclarados pela empresa (receita bruta, custos operacionais e referência à folha salarial já diagnosticada pela E-2) e aplica os parâmetros públicos de divisão para calcular o excedente disponível e a projeção dos fluxos possíveis.

A divisão de referência é 40% para caixa interno (reinvestimento), 40% para retorno ao sistema (fomento) e 20% para distribuição igualitária entre todos os trabalhadores. Valores iniciais, a calibrar com dados reais. Nenhum cálculo é sigiloso. A metodologia é pública e qualquer pessoa pode replicar a simulação com os mesmos dados de entrada.

O excedente retorna ao sistema como fluxo único para o fomento. Não há rateio por unidade cívica. A unidade cívica vê o resultado do excedente como organizações criadas, obras e serviços, não como caixa recebido. O cruzamento do retorno ao sistema com o território é responsabilidade da E-4 (Fase 2).

O valor está na visibilidade: a empresa que declara abre a caixa-preta e demonstra o que seria possível. Cada simulação publicada é um dado público. Pode ser comparada com outras empresas do mesmo porte e setor e agregada para análises territoriais.

No MVP, a simulação opera sobre dados autodeclarados sem verificação cruzada com bases externas. O próprio sistema de transparência radical é o mecanismo de pressão por veracidade.

A E-3 consome eventos do barramento para manter projeções locais e expõe a face BFF da submissão de balanço (POST /api/empresas/:id/balanco) e da leitura pública das simulações (GET /api/empresas/:id/simulacoes).


Especificação original em Apêndice B - Colônias.md, seção “Colônia E-3 — Simulação Econômica”, expandida e detalhada neste documento.


A E-3 é um módulo NestJS com encapsulamento próprio dentro do monolito modular do MVP. Consome quatro tipos de evento com cursor e replay (empresa.cadastrada, empresa.folha_submetida, empresa.diagnóstico_salarial_publicado e empresa.balanço_submetido) e mantém um handler stub para parâmetros.atualizados. Publica empresa.balanço_submetido pela face BFF e empresa.simulação_econômica_publicada pelo serviço de simulação.

src/empresa/e-3-simulacao-economica/
├── e3.module.ts # Module definition + OnModuleInit
├── e3.constants.ts # Tipos de evento, limites, versão de parâmetros, versão do schema
├── controllers/
│ ├── balanco.controller.ts # POST /api/empresas/:id/balanco — face BFF
│ └── simulacao.controller.ts # GET /api/empresas/:id/simulacoes — histórico público
├── guards/
│ └── auth.guard.ts # AuthGuard JWT da submissão de balanço
├── dto/
│ ├── submeter-balanco.dto.ts
│ ├── balanco-response.dto.ts
│ ├── listar-simulacoes.dto.ts
│ └── simulacao-response.dto.ts
├── services/
│ ├── simulacao.service.ts # Handler de empresa.balanço_submetido + varredura de pendentes
│ ├── balanco.service.ts # Face BFF: valida, persiste e publica o balanço
│ ├── consulta-simulacoes.service.ts # Listagem pública paginada
│ ├── algoritmo-excedente.service.ts # Algoritmo puro de excedente + divisão
│ ├── parametros.service.ts # Stub de parâmetros.atualizados + parâmetros estáticos
│ ├── projecao-empresas.service.ts # Handler de empresa.cadastrada + replay
│ └── projecao-folha.service.ts # Handlers de folha e diagnóstico + replay
├── repositories/
│ ├── empresa-projecao.repository.ts
│ ├── folha-diagnostico-projecao.repository.ts
│ ├── balanco-submissao.repository.ts
│ ├── balanco-pendente.repository.ts
│ ├── simulacao.repository.ts
│ └── consumer-offset.repository.ts
└── config/
└── parametros.config.ts # Parâmetros de divisão do excedente (40/40/20, ponto de partida)

Não há diretório entities/. Os specs ficam ao lado dos arquivos que testam.

@Module({
imports: [],
controllers: [BalancoController, SimulacaoController],
providers: [
BalancoService,
ConsultaSimulacoesService,
SimulacaoService,
AlgoritmoExcedenteService,
ParametrosService,
ProjecaoEmpresasService,
ProjecaoFolhaService,
EmpresaProjecaoRepository,
FolhaDiagnosticoProjecaoRepository,
BalancoSubmissaoRepository,
BalancoPendenteRepository,
SimulacaoRepository,
ConsumerOffsetRepository,
],
exports: [],
})
export class E3Module implements OnModuleInit {
constructor(
private readonly projecaoEmpresas: ProjecaoEmpresasService,
private readonly projecaoFolha: ProjecaoFolhaService,
private readonly simulacao: SimulacaoService,
private readonly parametros: ParametrosService,
private readonly balanco: BalancoService,
) {}
async onModuleInit(): Promise<void> {
await this.projecaoEmpresas.iniciar();
await this.projecaoFolha.iniciar();
await this.simulacao.iniciar();
await this.parametros.iniciar();
await this.balanco.iniciar();
}
}
  • O módulo não é @Global(). A E-3 não é dependência de nenhuma outra colônia. Outras colônias consomem seus eventos, não seu código.
  • O módulo não importa EventBusModule explicitamente. EventBusModule é @Global(), e o EventBusService é injetável sem import.
  • O módulo não importa RegistryModule. A validação de schema dos eventos publicados é feita pelo próprio Event Bus (N-0a) no momento da publicação.
  • O OnModuleInit inicializa os cinco serviços em sequência: projeção de empresas, projeção de folha, simulação, parâmetros e face BFF. Cada serviço faz o seed do cursor, o replay dos eventos do seu tipo e o registro dos consumidores; a simulação ainda faz as varreduras de órfãos e pendentes, e a face BFF republica os balanços sem evento publicado. A ordem importa: as projeções devem estar atualizadas antes de o handler de simulação começar a receber eventos.
  • O AlgoritmoExcedenteService é um serviço puro, sem dependências externas. Recebe números e parâmetros, retorna o resultado da simulação. Testável isoladamente com zero mocks.
  • O AuthGuard da face BFF valida o JWT com jsonwebtoken e o JWT_SECRET compartilhado com a D-1a. O sub do token é o representante_id da empresa.

Os serviços não declaram interfaces I*. Os contratos são os métodos públicos de cada classe:

class SimulacaoService {
iniciar(): Promise<void>;
registrarConsumidores(): void;
onBalancoSubmetido(evento: EventoConsultado): Promise<void>;
processarBalanco(dados: BalancoDados): Promise<void>;
processarPendentesDaFolha(submissaoId: string): Promise<void>;
processarPendentesDaEmpresa(empresaId: string): Promise<void>;
}
class BalancoService {
iniciar(): Promise<void>;
submeter(empresaId: string, dto: SubmeterBalancoDto, representanteId: string): Promise<RespostaSubmissaoBalanco>;
republicarBalancosOrfaos(): Promise<void>;
}
class ConsultaSimulacoesService {
listar(empresaId: string, dto: ListarSimulacoesDto): Promise<HistoricoSimulacoesDto>;
}
class AlgoritmoExcedenteService {
calcular(receitaBruta: Decimal, custosOperacionais: Decimal,
folhaReequilibrada: Decimal, qtdTrabalhadores: number,
params: DivisaoParams): SimulacaoResultado;
}
class ParametrosService {
iniciar(): void;
getDivisaoParams(): DivisaoParams;
getVersao(): string;
onParametrosAtualizados(evento: EventoConsultado): Promise<void>;
}
class ProjecaoEmpresasService {
iniciar(): Promise<void>;
registrarConsumidores(): void;
onEmpresaCadastrada(evento: EventoConsultado): Promise<void>;
}
class ProjecaoFolhaService {
iniciar(): Promise<void>;
registrarConsumidores(): void;
onFolhaSubmetida(evento: EventoConsultado): Promise<void>;
onDiagnosticoSalarialPublicado(evento: EventoConsultado): Promise<void>;
}

Os tipos exportados pelos repositórios (EmpresaProjecaoRegistro, FolhaProjecaoRegistro, BalancoSubmissaoRegistro, BalancoPendenteRegistro, SimulacaoPublicavel, SimulacaoListada, OffsetRegistro) fazem o papel das entidades Prisma.

A E-3 mantém duas projeções locais, populadas assincronamente por eventos de outras colônias:

  • Empresas: empresa.cadastrada (E-1) — dados cadastrais básicos, status e representante
  • Folha e diagnóstico: empresa.folha_submetida + empresa.diagnóstico_salarial_publicado (E-2) — total de colaboradores e custo da folha reequilibrada

Os parâmetros de divisão não são projeção. São carregados de config/parametros.config.ts e servidos por ParametrosService. O handler de parâmetros.atualizados existe como stub e apenas registra o evento no log.

A E-3 mantém uma tabela de balanços pendentes (e3.balancos_pendentes) para cenários em que o balanço chega antes da folha ou do diagnóstico salarial. O processamento é reativado quando a folha ou o diagnóstico chega e pela varredura periódica de 60 segundos.

A face BFF da colônia recebe a submissão de balanço, persiste em e3.balancos_submissoes e publica empresa.balanço_submetido. A leitura pública das simulações é servida pelo GET /api/empresas/:id/simulacoes, que espelha o payload do evento, sem os campos internos.


Todas as tabelas da E-3 residem no schema e3 do PostgreSQL. Este schema é de uso exclusivo do módulo E-3. Nenhuma outra colônia lê ou escreve nestas tabelas.

Projeção local de empresas, populada pelo consumidor de empresa.cadastrada. Contém os campos relevantes para a E-3 contextualizar a simulação e autorizar a submissão de balanço sem depender de consulta à E-1.

CREATE SCHEMA IF NOT EXISTS e3;
CREATE TABLE e3.empresas_projecao (
empresa_id UUID PRIMARY KEY,
razao_social VARCHAR(300) NOT NULL,
porte VARCHAR(20) NOT NULL,
setor_id VARCHAR(20) NOT NULL,
representante_id UUID,
status VARCHAR(30) NOT NULL DEFAULT 'cadastrada',
criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW()
);
CREATE INDEX empresas_projecao_status_idx ON e3.empresas_projecao (status);
CREATE INDEX empresas_projecao_setor_id_idx ON e3.empresas_projecao (setor_id);
CREATE INDEX empresas_projecao_representante_id_idx ON e3.empresas_projecao (representante_id);

Sem CHECKs no banco. A lista de status válidos é validada em aplicação no handler, que descarta o evento com log quando o valor é desconhecido.

Coluna Tipo Descrição
empresa_id UUID PK Identificador da empresa. Extraído do payload de empresa.cadastrada.
razao_social VARCHAR(300) NOT NULL Nome oficial da organização. Usado para contexto na simulação.
porte VARCHAR(20) NOT NULL micro, pequena, media ou grande, como declarado no evento.
setor_id VARCHAR(20) NOT NULL Setor CNAE macro. Para segmentação em análises agregadas futuras.
representante_id UUID Representante que cadastrou a empresa. Comparado ao sub do JWT na submissão de balanço. Nulo quando o evento não traz o campo ou o valor não é UUID; nesse caso a submissão fica bloqueada.
status VARCHAR(30) NOT NULL cadastrada (default), socializada, pendente ou recusada. Validado em aplicação.
criado_em TIMESTAMPTZ(2) Timestamp de criação.
atualizado_em TIMESTAMPTZ(2) Timestamp da última atualização.

Projeção local que consolida dados de duas fontes de eventos da E-2: empresa.folha_submetida (total de colaboradores e submissao_id) e empresa.diagnóstico_salarial_publicado (custo total após reequilíbrio). Cada linha é populada incrementalmente: a submissão cria a linha, o diagnóstico a atualiza.

CREATE TABLE e3.folhas_projecao (
submissao_id UUID PRIMARY KEY,
empresa_id UUID NOT NULL,
total_colaboradores INTEGER NOT NULL DEFAULT 0,
custo_total_depois NUMERIC(15,2),
diagnostico_id UUID,
diagnostico_publicado_em TIMESTAMPTZ(2),
versao_parametros_diag VARCHAR(20),
criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW()
);
CREATE INDEX folhas_projecao_empresa_id_idx ON e3.folhas_projecao (empresa_id);

Sem CHECKs no banco. O handler de simulação trata total_colaboradores <= 0 como projeção incompleta e mantém o balanço pendente.

Coluna Tipo Descrição
submissao_id UUID PK Identificador da submissão de folha. Vem de empresa.folha_submetida.
empresa_id UUID NOT NULL Empresa que submeteu a folha. Denormalizado para queries sem JOIN.
total_colaboradores INTEGER NOT NULL Total de colaboradores declarados. Usado para calcular valor_por_trabalhador. Default 0 até a submissão chegar.
custo_total_depois NUMERIC(15,2) Custo total da folha após redistribuição. Vem de empresa.diagnóstico_salarial_publicado. Nulo até o diagnóstico ser publicado.
diagnostico_id UUID Id do diagnóstico. Vem do event_id de empresa.diagnóstico_salarial_publicado.
diagnostico_publicado_em TIMESTAMPTZ(2) Timestamp de quando o diagnóstico foi publicado.
versao_parametros_diag VARCHAR(20) Versão dos parâmetros usados no diagnóstico. Para rastreabilidade.
criado_em TIMESTAMPTZ(2) Timestamp de criação da linha (quando empresa.folha_submetida foi consumido).
atualizado_em TIMESTAMPTZ(2) Timestamp da última atualização (quando a submissão ou o diagnóstico foi consumido).

Registro da face BFF. Persiste cada balanço aceito antes da publicação do evento e sustenta a idempotência por hash de conteúdo e a varredura de submissões órfãs no boot.

CREATE TABLE e3.balancos_submissoes (
id UUID PRIMARY KEY,
empresa_id UUID NOT NULL,
receita_bruta NUMERIC(15,2) NOT NULL,
custos_operacionais NUMERIC(15,2) NOT NULL,
referencia_submissao_folha UUID NOT NULL,
periodo_inicio DATE NOT NULL,
periodo_fim DATE NOT NULL,
idempotencia_hash VARCHAR(64) NOT NULL,
evento_publicado_em TIMESTAMPTZ(2),
criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX balancos_submissoes_idempotencia_hash_key ON e3.balancos_submissoes (idempotencia_hash);
CREATE INDEX balancos_submissoes_empresa_id_idx ON e3.balancos_submissoes (empresa_id);
CREATE INDEX balancos_submissoes_evento_publicado_em_pendente_idx
ON e3.balancos_submissoes (criado_em)
WHERE evento_publicado_em IS NULL;

O índice da varredura de órfãos é parcial e vive na migration, porque o Prisma não o representa no schema.

Coluna Tipo Descrição
id UUID PK submissao_id da submissão. Gerado pela aplicação. Também é o event_id do evento publicado.
empresa_id UUID NOT NULL Empresa que submeteu o balanço.
receita_bruta NUMERIC(15,2) NOT NULL Receita bruta declarada.
custos_operacionais NUMERIC(15,2) NOT NULL Custos operacionais declarados, sem folha.
referencia_submissao_folha UUID NOT NULL submissao_id da folha referenciada.
periodo_inicio / periodo_fim DATE NOT NULL Período contábil de referência.
idempotencia_hash VARCHAR(64) NOT NULL UNIQUE SHA-256 de empresa, representante, valores, folha referenciada e período. Duplicata retorna 409 com o submissao_id existente.
evento_publicado_em TIMESTAMPTZ(2) Preenchido após a publicação bem-sucedida de empresa.balanço_submetido. Nulo indica submissão órfã, republicada no boot.
criado_em TIMESTAMPTZ(2) Timestamp de criação.

Armazena balanços consumidos cuja empresa, folha ou diagnóstico ainda não estão disponíveis na projeção. Quando o dado chega, o balanço é reprocessado automaticamente.

CREATE TABLE e3.balancos_pendentes (
evento_id UUID PRIMARY KEY,
empresa_id UUID NOT NULL,
submissao_id UUID NOT NULL,
receita_bruta NUMERIC(15,2) NOT NULL,
custos_operacionais NUMERIC(15,2) NOT NULL,
referencia_submissao_folha UUID NOT NULL,
periodo_inicio DATE NOT NULL,
periodo_fim DATE NOT NULL,
evento_sequence BIGINT NOT NULL,
criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW()
);
CREATE INDEX balancos_pendentes_referencia_submissao_folha_idx
ON e3.balancos_pendentes (referencia_submissao_folha);
CREATE INDEX balancos_pendentes_empresa_id_idx ON e3.balancos_pendentes (empresa_id);

Sem CHECKs no banco. Os valores não negativos são validados na extração do payload, antes de o balanço ser armazenado.

Coluna Tipo Descrição
evento_id UUID PK event_id do empresa.balanço_submetido. Chave de idempotência.
empresa_id UUID NOT NULL Empresa que submeteu o balanço.
submissao_id UUID NOT NULL submissao_id do balanço.
receita_bruta NUMERIC(15,2) NOT NULL Receita bruta declarada.
custos_operacionais NUMERIC(15,2) NOT NULL Custos operacionais declarados.
referencia_submissao_folha UUID NOT NULL submissao_id da folha referenciada. Usado para o lookup quando o diagnóstico chega.
periodo_inicio / periodo_fim DATE NOT NULL Período contábil de referência.
evento_sequence BIGINT NOT NULL Sequence number do evento no barramento. Para rastreabilidade.
criado_em TIMESTAMPTZ(2) Timestamp de criação.

Resultado do processamento da simulação econômica. Contém o breakdown completo: excedente calculado, divisão parametrizada, valores por destino e payload exato publicado.

CREATE TABLE e3.simulacoes (
id UUID PRIMARY KEY,
balanco_evento_id UUID NOT NULL,
balanco_submissao_id UUID NOT NULL,
empresa_id UUID NOT NULL,
receita_declarada NUMERIC(15,2) NOT NULL,
custos_declarados NUMERIC(15,2) NOT NULL,
folha_reequilibrada NUMERIC(15,2) NOT NULL,
excedente_calculado NUMERIC(15,2) NOT NULL,
total_colaboradores INTEGER NOT NULL,
percentual_fomento NUMERIC(5,2) NOT NULL,
percentual_reinvestimento NUMERIC(5,2) NOT NULL,
percentual_trabalhadores NUMERIC(5,2) NOT NULL,
valor_fomento_total NUMERIC(15,2) NOT NULL DEFAULT 0,
valor_reinvestimento NUMERIC(15,2) NOT NULL DEFAULT 0,
valor_trabalhadores_total NUMERIC(15,2) NOT NULL DEFAULT 0,
valor_por_trabalhador NUMERIC(15,2) NOT NULL DEFAULT 0,
referencia_submissao_folha UUID NOT NULL,
diagnostico_id UUID NOT NULL,
periodo_inicio DATE NOT NULL,
periodo_fim DATE NOT NULL,
versao_parametros VARCHAR(20) NOT NULL,
payload_simulacao JSONB NOT NULL,
evento_publicado_em TIMESTAMPTZ(2),
criado_em TIMESTAMPTZ(2) NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX simulacoes_balanco_evento_id_key ON e3.simulacoes (balanco_evento_id);
CREATE UNIQUE INDEX simulacoes_balanco_submissao_id_key ON e3.simulacoes (balanco_submissao_id);
CREATE INDEX simulacoes_empresa_id_idx ON e3.simulacoes (empresa_id);
CREATE INDEX simulacoes_empresa_id_criado_em_idx ON e3.simulacoes (empresa_id, criado_em DESC);
CREATE INDEX simulacoes_diagnostico_id_idx ON e3.simulacoes (diagnostico_id);
CREATE INDEX simulacoes_evento_publicado_em_pendente_idx
ON e3.simulacoes (criado_em)
WHERE evento_publicado_em IS NULL;

O índice da varredura de órfãos é parcial e vive na migration, porque o Prisma não o representa no schema.

Sem CHECK no banco para a soma dos percentuais. A soma é garantida pela configuração dos parâmetros em aplicação.

Coluna Tipo Descrição
id UUID PK simulacao_id. Gerado pela aplicação. Também é o event_id do evento publicado.
balanco_evento_id UUID NOT NULL UNIQUE event_id do empresa.balanço_submetido que originou esta simulação. Chave de idempotência.
balanco_submissao_id UUID NOT NULL UNIQUE submissao_id do balanço. Chave alternativa de idempotência.
empresa_id UUID NOT NULL Empresa da simulação. Denormalizado para queries.
receita_declarada NUMERIC(15,2) NOT NULL Receita bruta declarada no balanço.
custos_declarados NUMERIC(15,2) NOT NULL Custos operacionais declarados.
folha_reequilibrada NUMERIC(15,2) NOT NULL Custo total da folha após redistribuição (custo_total_depois do diagnóstico da E-2).
excedente_calculado NUMERIC(15,2) NOT NULL receita - custos - folha_reequilibrada. Pode ser zero ou negativo.
total_colaboradores INTEGER NOT NULL Total de colaboradores para cálculo do valor por trabalhador.
percentual_fomento NUMERIC(5,2) NOT NULL Percentual de retorno ao sistema (fomento).
percentual_reinvestimento NUMERIC(5,2) NOT NULL Percentual de caixa interno (reinvestimento).
percentual_trabalhadores NUMERIC(5,2) NOT NULL Percentual para distribuição igualitária.
valor_fomento_total NUMERIC(15,2) NOT NULL Valor total de retorno ao sistema. 0 se excedente <= 0.
valor_reinvestimento NUMERIC(15,2) NOT NULL Valor total de caixa interno. 0 se excedente <= 0.
valor_trabalhadores_total NUMERIC(15,2) NOT NULL Valor total destinado a trabalhadores. 0 se excedente <= 0.
valor_por_trabalhador NUMERIC(15,2) NOT NULL Valor igualitário por trabalhador. 0 se excedente <= 0 ou total_colaboradores = 0.
referencia_submissao_folha UUID NOT NULL submissao_id da folha usada como base. Para rastreabilidade.
diagnostico_id UUID NOT NULL Id do diagnóstico da E-2. Para rastreabilidade.
periodo_inicio / periodo_fim DATE NOT NULL Período contábil da simulação, em duas colunas DATE.
versao_parametros VARCHAR(20) NOT NULL Versão dos parâmetros usados. No MVP: string fixa "mvp-v1".
payload_simulacao JSONB NOT NULL Payload exato publicado em empresa.simulação_econômica_publicada. Para auditoria e replay.
evento_publicado_em TIMESTAMPTZ(2) Preenchido após publicação bem-sucedida do evento. Nulo indica simulação órfã, republicada no boot.
criado_em TIMESTAMPTZ(2) Timestamp de criação.

Controla o cursor de processamento para replay seletivo após falha ou reinicialização. Segue o padrão definido pela N-0a (Event Bus).

CREATE TABLE e3.consumer_offset (
tipo_evento VARCHAR(255) PRIMARY KEY,
last_sequence BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ(2) NOT NULL DEFAULT NOW()
);

A tabela contém quatro linhas no MVP, uma para cada tipo de evento com cursor: empresa.cadastrada, empresa.folha_submetida, empresa.diagnóstico_salarial_publicado e empresa.balanço_submetido. As linhas são semeadas no boot com obterMaiorSequence(), sem sobrescrever cursor existente. A estrutura com PK em tipo_evento permite extensão futura sem alteração de schema. parâmetros.atualizados é registrado como stub e não tem cursor.

Cinco migrations formam o schema e3:

  1. 20260813193220_create_e3_tables — cria o schema e3, as tabelas empresas_projecao, folhas_projecao, balancos_submissoes, balancos_pendentes, simulacoes e consumer_offset, com os índices. Na criação, simulacoes ainda tinha as colunas do rateio por UC (quantidade_ucs, percentual_uc, destinos_uc) e existia a tabela empresa_ucs.
  2. 20260818090000_add_representante_id_empresas_projecao — adiciona representante_id e o índice nas projeções de empresa da E-2 e da E-3.
  3. 20260818120000_backfill_representante_id_empresas_projecao — preenche representante_id das projeções a partir do payload de empresa.cadastrada no core.event_log. Registros sem evento correspondente permanecem nulos e bloqueados para submissão de balanço.
  4. 20260820170000_e3_excedente_fomento — renomeia percentual_uc para percentual_fomento e valor_uc_total para valor_fomento_total, remove quantidade_ucs e destinos_uc de simulacoes e dropa empresa_ucs. O excedente deixa de ser rateado por UC e retorna ao sistema como fluxo único.
  5. 20260918180000_e3_varredura_orfaos_indices — cria os índices parciais de evento_publicado_em para as varreduras de órfãos de balancos_submissoes e simulacoes.

As projeções não têm seed por migration. São populadas pelo replay dos eventos no boot.

O schema e3 não tem foreign keys internas. Todas as tabelas são projeções independentes:

  • e3.empresas_projecao — alimentada por empresa.cadastrada
  • e3.folhas_projecao — alimentada por empresa.folha_submetida + empresa.diagnóstico_salarial_publicado
  • e3.balancos_submissoes — alimentada pela face BFF
  • e3.balancos_pendentes — alimentada por empresa.balanço_submetido quando o processamento é bloqueado
  • e3.simulacoes — alimentada pelo algoritmo de simulação

Não há FK entre e3.simulacoes e e3.folhas_projecao ou e3.empresas_projecao. A integridade é garantida em aplicação via consumo sequencial dos eventos. FKs entre schemas de colônias distintas são proibidas pela regra de isolamento.

e3.folhas_projecao unifica dois eventos da E-2 em uma tabela. A alternativa (duas tabelas separadas: folhas_submissoes + diagnosticos) criaria JOINs desnecessários para a query mais frequente da E-3: “dado um submissao_id, qual o custo_total_depois e total_colaboradores?”. O UPSERT incremental, com INSERT na submissão e UPDATE no diagnóstico, é simples e evita estado de consistência entre tabelas. O campo custo_total_depois é nullable até o diagnóstico chegar.

e3.balancos_pendentes como tabela separada, não como coluna de status em e3.simulacoes. Um balanço pendente não gerou simulação ainda e não pertence à tabela de simulações. A separação mantém e3.simulacoes como registro exclusivo de simulações concluídas e publicadas. O índice em referencia_submissao_folha permite lookup O(log n) quando o diagnóstico chega: “quais balanços estavam esperando esta folha?”.

Sem rateio territorial: valor_fomento_total como fluxo único. O excedente retorna ao sistema como um valor único, sem divisão por UC. Não há destinos_uc, não há quantidade_ucs e não há tabela e3.empresa_ucs. O cruzamento do retorno ao sistema com o território é responsabilidade da E-4 (Fase 2), que consome a simulação e as demandas do ranking.

NUMERIC(15,2) para todos os valores monetários. Mesmo padrão da E-2. Precisão de centavos para valores de até 10 trilhões. A aritmética decimal exata via decimal.js garante que valor_fomento_total + valor_reinvestimento + valor_trabalhadores_total == excedente * (excedente > 0 ? 1 : 0) ao centavo.

Duas colunas DATE para o período contábil. O Prisma não suporta DATERANGE. O período é armazenado em periodo_inicio e periodo_fim. Queries de contenção e sobreposição ficam para a Fase 2, se necessárias.

valor_fomento_total calculado no momento da simulação, não recalculado. O valor do retorno ao sistema é um snapshot dos parâmetros vigentes no momento do cálculo. Para recálculo de simulações existentes com novos parâmetros, o handler de parâmetros.atualizados (Fase 2) será responsável.

representante_id na projeção de empresas. A submissão de balanço exige que o sub do JWT seja o representante que cadastrou a empresa. Sem o dado na projeção, a submissão é recusada. É uma guarda fail-closed.

Sem CHECKs no banco. As listas de status, os valores não negativos e o total de colaboradores são validados em aplicação, com descarte e log. Os handlers aceitam eventos fora de ordem e a decisão de descarte é do fluxo, não do banco.

Idempotência da submissão por hash de conteúdo. O hash SHA-256 cobre empresa, representante, valores, folha referenciada e período, sem componente temporal. A mesma submissão retorna 409 permanente com o submissao_id existente.


A E-3 produz dois tipos de evento e consome quatro com cursor e replay, mais um stub. Os schemas completos (JSON Schema draft-2020-12) estão definidos no Registry (N-0b). Esta seção descreve os contratos do ponto de vista da E-3.

3.1 Evento produzido pela face BFF: empresa.balanço_submetido

Seção intitulada “3.1 Evento produzido pela face BFF: empresa.balanço_submetido”
Propriedade Valor
Tipo empresa.balanço_submetido
Schema version 1.0.0
Produtor E-3 (face BFF, BalancoController)
Consumidor E-3 (gatilho de processamento)
Descrição Empresa submeteu dados financeiros para simulação de excedente. Autodeclarado.

Payload publicado:

interface EmpresaBalancoSubmetidoPayload {
empresa_id: string; // UUID v4
submissao_id: string; // UUID v4
receita_bruta: number; // >= 0
custos_operacionais: number; // >= 0
referencia_submissao_folha: string; // UUID v4 — submissao_id da folha base
periodo_referencia: {
inicio: string; // ISO-8601 date
fim: string; // ISO-8601 date
};
}

A submissão usa o submissao_id como event_id e como correlacao_id. A publicação usa publicarComRetry e só depois marca evento_publicado_em na linha de e3.balancos_submissoes. Submissões com evento_publicado_em nulo são republicadas na varredura do boot. O DTO de entrada exige periodo_referencia; o schema do Registry mantém o campo como opcional, e a E-3 descarta com log o balanço sem período válido.

3.2 Evento produzido: empresa.simulação_econômica_publicada

Seção intitulada “3.2 Evento produzido: empresa.simulação_econômica_publicada”
Propriedade Valor
Tipo empresa.simulação_econômica_publicada
Schema version 2.0.0
Produtor E-3 (esta colônia)
Consumidores E-4 (Impacto Territorial, Fase 2) e a leitura pública servida pela própria E-3
Descrição Resultado da simulação de excedente: valor calculado, divisão parametrizada (caixa interno, trabalhadores, retorno ao sistema) e valor por trabalhador. A versão 1.0.0, com quantidade_ucs e destinos_uc, permanece no catálogo do Registry.

Payload publicado:

interface EmpresaSimulacaoEconomicaPublicadaPayload {
empresa_id: string; // UUID v4
submissao_id: string; // UUID v4 — submissao_id do balanço
receita_declarada: number; // >= 0
custos_declarados: number; // >= 0
folha_reequilibrada: number; // >= 0 — custo_total_depois do diagnóstico
excedente_calculado: number; // receita - custos - folha_reequilibrada. Pode ser zero ou negativo.
total_colaboradores: number; // > 0, integer
divisao: {
percentual_fomento: number; // 0–100 (ex: 40.00) — retorno ao sistema
percentual_reinvestimento: number; // 0–100 (ex: 40.00) — caixa interno
percentual_trabalhadores: number; // 0–100 (ex: 20.00)
valor_fomento_total: number; // >= 0
valor_reinvestimento: number; // >= 0
valor_trabalhadores_total: number; // >= 0
valor_por_trabalhador: number; // >= 0 — valor_trabalhadores_total / total_colaboradores
};
referencia_submissao_folha: string; // UUID v4 — submissao_id da folha base
diagnostico_id: string; // UUID v4 — diagnostico_id da E-2
periodo_referencia: { // Período contábil da simulação
inicio: string; // ISO-8601 date (YYYY-MM-DD)
fim: string; // ISO-8601 date (YYYY-MM-DD)
};
versao_parametros: string; // "mvp-v1" no MVP
}

O schema do Registry (N-0b, seção 3.4.27) define a versão 2.0.0 com este payload. A E-3 publica com versao_schema: '2.0.0'. O event_id é o simulacao_id e o correlacao_id é o submissao_id.

Propriedade Valor
Tipo empresa.cadastrada
Schema version 1.0.0
Produtor E-1 (Cadastro Institucional)
Consumidor E-3 (esta colônia — projeção local)
Descrição Nova empresa registrada. A E-3 insere ou atualiza e3.empresas_projecao.

Payload esperado (conforme o schema empresa.cadastrada do Registry N-0b):

interface EmpresaCadastradaPayload {
empresa_id: string;
razao_social: string;
porte: string; // 'micro' | 'pequena' | 'media' | 'grande'
setor_id: string;
representante_id?: string; // UUID do representante autenticado
status: string; // 'cadastrada' | 'socializada' | 'pendente' | 'recusada'
// demais campos ignorados pela E-3
}

A E-3 extrai empresa_id, razao_social, porte, setor_id, status e representante_id. Os demais campos são ignorados. O status ausente assume cadastrada; um valor fora da lista de quatro status válidos descarta o evento com log. O representante_id só entra na projeção quando é um UUID válido.

Propriedade Valor
Tipo empresa.folha_submetida
Schema version 1.0.0
Produtor E-2 (face BFF da folha)
Consumidor E-3 (esta colônia — projeção local de folha)
Descrição Folha salarial submetida. A E-3 extrai submissao_id, empresa_id e total_colaboradores.

Payload esperado (conforme o schema empresa.folha_submetida do Registry N-0b):

interface EmpresaFolhaSubmetidaPayload {
empresa_id: string;
submissao_id: string;
total_colaboradores: number; // >= 1 — obrigatório no Registry
quantidade_cargos: number; // >= 1
cargos_hash: string; // SHA-256 dos cargos; a E-3 ignora
}

A E-3 extrai apenas submissao_id, empresa_id e total_colaboradores. O total é truncado para inteiro e precisa ser maior ou igual a 1; abaixo disso o evento é descartado com log e o cursor avança. quantidade_cargos e cargos_hash são ignorados.

3.5 Evento consumido: empresa.diagnóstico_salarial_publicado

Seção intitulada “3.5 Evento consumido: empresa.diagnóstico_salarial_publicado”
Propriedade Valor
Tipo empresa.diagnóstico_salarial_publicado
Schema version 1.0.0
Produtor E-2 (Transparência Salarial e Folha)
Consumidor E-3 (esta colônia — atualiza a projeção de folha e desbloqueia balanços pendentes)
Descrição Diagnóstico salarial publicado. A E-3 extrai custo_total_depois e atualiza e3.folhas_projecao.

Payload esperado (conforme o schema empresa.diagnóstico_salarial_publicado do Registry N-0b):

interface EmpresaDiagnosticoSalarialPublicadoPayload {
empresa_id: string;
submissao_id: string;
menor_salario: number;
maior_salario: number;
razao_atual: number;
razao_parametrizada: number;
custo_total_antes: number;
custo_total_depois: number;
redistribuicao_total: number;
colaboradores_acima_teto: number;
colaboradores_abaixo_minimo: number;
versao_parametros: string;
}

A E-3 extrai empresa_id, submissao_id, custo_total_depois e versao_parametros. Os demais campos são ignorados. O diagnostico_id é o event_id do próprio evento. O custo_total_depois não pode ser negativo; o versao_parametros ausente assume desconhecida.

3.6 Evento consumido (stub): parâmetros.atualizados

Seção intitulada “3.6 Evento consumido (stub): parâmetros.atualizados”
Propriedade Valor
Tipo parâmetros.atualizados
Schema version 1.0.0
Produtor Sistema de parametrização (MVP: configuração estática; Fase 3: governança de parâmetros)
Consumidor E-3 (esta colônia — stub no MVP)
Descrição Parâmetros do sistema atualizados. No MVP, o handler loga e ignora.

Payload esperado (conforme o schema parâmetros.atualizados do Registry N-0b):

interface ParametrosAtualizadosPayload {
versao: string; // semver
parametros: {
pesos_nacionais: Record<string, number>;
scores_horizontais: Record<string, number>;
razao_salarial_maxima: number;
divisao_excedente: {
fomento: number;
reinvestimento: number;
trabalhadores: number;
};
// demais campos
};
motivo?: string;
}

No MVP, este evento não é publicado por nenhuma colônia. O handler registra o listener no barramento, loga o event_id e a versão recebida, e não recalcula nada. Os parâmetros de divisão do excedente são carregados de config/parametros.config.ts.

3.7 Ordem de operações: consumir empresa.balanço_submetido

Seção intitulada “3.7 Ordem de operações: consumir empresa.balanço_submetido”

O fluxo em SimulacaoService.onBalancoSubmetido() segue esta ordem exata:

1. Verificar idempotência
→ buscar simulação por balanco_evento_id = event.event_id
→ se encontrada: log("Simulação já existe para este balanço"), avançar cursor, retornar
→ buscar balanço pendente por evento_id = event.event_id
→ se encontrado: log("Balanço já está pendente"), avançar cursor, retornar
2. Extrair e validar o payload
→ extrair empresa_id, submissao_id, receita_bruta, custos_operacionais,
referencia_submissao_folha e periodo_referencia
→ payload incompleto ou período ausente/invertido: log.error, avançar cursor, retornar
→ receita_bruta < 0 ou custos_operacionais < 0: log.error, avançar cursor, retornar
3. Validar a empresa
→ buscar em e3.empresas_projecao por empresa_id
→ se não encontrada: armazenar pendente, log.warn, avançar cursor, retornar
→ se status fora de ('cadastrada', 'socializada'): log, avançar cursor, retornar
4. Verificar disponibilidade da folha e do diagnóstico
→ buscar e3.folhas_projecao por referencia_submissao_folha
→ folha ausente: armazenar pendente, log("Balanço pendente — aguardando folha"),
avançar cursor, retornar
→ custo_total_depois nulo: armazenar pendente,
log("Balanço pendente — aguardando diagnóstico salarial"), avançar cursor, retornar
→ total_colaboradores <= 0: armazenar pendente,
log("Balanço pendente — aguardando total de colaboradores"), avançar cursor, retornar
5. Processar
→ processarBalanco(dados)

O armazenamento como pendente respeita o limite de 10 balanços por empresa. Acima do limite, o balanço é descartado com log.warn e o cursor avança.

3.8 Ordem de operações: desbloquear balanços pendentes

Seção intitulada “3.8 Ordem de operações: desbloquear balanços pendentes”

Quando empresa.diagnóstico_salarial_publicado chega, o ProjecaoFolhaService atualiza e3.folhas_projecao e verifica e3.balancos_pendentes:

1. Atualizar a projeção de folha
→ UPDATE em e3.folhas_projecao por submissao_id com custo_total_depois,
diagnostico_id = event.event_id, diagnostico_publicado_em e versao_parametros_diag
→ se nenhuma linha afetada: INSERT com total_colaboradores = 0
(a folha ainda não foi submetida; o total é preenchido quando
empresa.folha_submetida chegar)
2. Processar os pendentes desta folha
→ processarPendentesDaFolha(submissao_id)
→ se a folha não está completa (custo ausente ou total <= 0): retornar sem processar
→ para cada pendente da folha, em ordem de criado_em:
→ processarBalanco(pendente)
→ falha no processamento: log.error e o pendente permanece
3. Casos equivalentes
→ empresa.folha_submetida chama processarPendentesDaFolha
→ empresa.cadastrada chama processarPendentesDaEmpresa, que só processa
os pendentes com folha completa
→ a varredura de 60 segundos e a do boot chamam a busca geral de pendentes
1. Extrair dados: empresa_id, razao_social, porte, setor_id, status, representante_id
2. Payload incompleto ou status inválido: log.error, avançar cursor, retornar
3. UPSERT em e3.empresas_projecao por empresa_id
4. processarPendentesDaEmpresa(empresa_id)
5. Avançar cursor
1. Extrair dados: submissao_id, empresa_id, total_colaboradores
2. Payload incompleto ou total < 1: log.error, avançar cursor, retornar
3. UPSERT em e3.folhas_projecao por submissao_id
4. processarPendentesDaFolha(submissao_id)
5. Avançar cursor
1. Extrair dados: submissao_id, empresa_id, custo_total_depois, versao_parametros
2. Payload incompleto ou custo negativo: log.error, avançar cursor, retornar
3. UPSERT do diagnóstico em e3.folhas_projecao (UPDATE e INSERT parcial)
4. processarPendentesDaFolha(submissao_id)
5. Avançar cursor

A E-3 implementa quatro camadas de idempotência:

Camada 1 — Projeções (UPSERT): Todas as tabelas de projeção usam UPSERT por chave primária. Eventos duplicados ou fora de ordem não geram linhas duplicadas. O atualizado_em registra a última atualização.

Camada 2 — Simulações (UNIQUE em balanco_evento_id e balanco_submissao_id): As constraints UNIQUE em e3.simulacoes impedem que o mesmo balanço gere duas simulações. Se o INSERT for recusado pela unicidade, o serviço garante a publicação do registro existente quando evento_publicado_em está nulo, remove o pendente e avança o cursor.

Camada 3 — Balanços pendentes (PK evento_id): A mesma chegada de empresa.balanço_submetido não cria duas linhas de pendente. O handler checa a existência antes e o repositório trata a violação de unicidade como false.

Camada 4 — Barramento (N-0a): O EventBusService.publicar() valida tipo, versão e formato antes do insert e usa o event_id como chave de idempotência. Para empresa.simulação_econômica_publicada, o event_id é o próprio simulacao_id; para empresa.balanço_submetido, é o submissao_id. A face BFF soma a idempotência de conteúdo por idempotencia_hash.

Cenário Comportamento
empresa.balanço_submetido duplicado (mesmo event_id) Simulação já existe. Log e cursor avança.
empresa.balanço_submetido com submissao_id já simulado e event_id diferente UNIQUE no INSERT. O serviço republica a simulação existente se evento_publicado_em estiver nulo, remove o pendente e avança o cursor.
Payload de balanço incompleto, período inválido ou valores negativos Descarte com log.error e cursor avançado.
Empresa não encontrada na projeção Balanço vai para e3.balancos_pendentes. Quando empresa.cadastrada chegar, processarPendentesDaEmpresa reprocessa.
Folha referenciada não encontrada na projeção Balanço vai para e3.balancos_pendentes. Quando empresa.folha_submetida chegar, processarPendentesDaFolha reprocessa.
Diagnóstico da folha ainda não publicado Balanço vai para e3.balancos_pendentes. Quando empresa.diagnóstico_salarial_publicado chegar, processarPendentesDaFolha reprocessa.
Limite de 10 pendentes por empresa atingido Descarte com log.warn e cursor avançado.
Excedente <= 0 (receita não cobre custos + folha) Simulação publicada normalmente. valor_fomento_total, valor_reinvestimento, valor_trabalhadores_total e valor_por_trabalhador são 0. A simulação demonstra “não há excedente”; isso é informação pública relevante.
INSERT em e3.simulacoes falha por erro de infraestrutura Exceção propagada, cursor não avança e o evento vai para a DLQ. Nenhum evento publicado.
Publicação de empresa.simulação_econômica_publicada falha após as tentativas do retry Exceção propagada, cursor não avança e o evento vai para a DLQ. A simulação fica persistida com evento_publicado_em nulo e é republicada pela varredura do boot.
Publicação de empresa.balanço_submetido falha após as tentativas do retry HTTP 500 na submissão. A linha fica em e3.balancos_submissoes com evento_publicado_em nulo e é republicada pela varredura do boot.
Evento de projeção chega em rajada (burst) UPSERT é idempotente. O processamento sequencial pelo barramento garante que o último estado prevalece.
parâmetros.atualizados chega no MVP Handler stub: log e ignora. Sem efeito colateral.

A face BFF vive dentro da própria E-3. O formulário de balanço tem quatro campos (receita, custos, referência à folha e período). Como nenhum outro módulo publicaria empresa.balanço_submetido, o repo implementa a face BFF dentro da própria E-3, com o mesmo precedente da E-2. O POST /api/empresas/:id/balanco valida, persiste em e3.balancos_submissoes com idempotência por hash, publica com retry e responde 202 Accepted com { submissao_id, status: 'aceito' }.

Balanços pendentes como mecanismo de resiliência à ordem de eventos. No sistema orientado a eventos, não há garantia de que empresa.diagnóstico_salarial_publicado chegue antes de empresa.balanço_submetido. A alternativa de rejeitar o balanço (HTTP 400) delegaria o problema ao usuário, que precisaria voltar depois. A fila interna na tabela e3.balancos_pendentes é transparente para o usuário e resolve a ordenação sem polling do lado do cliente. A varredura de 60 segundos e a do boot cobrem o caso de eventos que não chegam.

UPSERT sem FK entre projeções. As projeções e3.folhas_projecao e e3.empresas_projecao não têm foreign key entre si. Eventos podem chegar em qualquer ordem. O UPSERT trata cada evento de forma independente. A consistência eventual é garantida pelo barramento: todos os eventos, na ordem correta, eventualmente serão processados. A integridade referencial é verificada em aplicação, não no banco. Mesmo padrão da E-2.

O retorno ao sistema não tem rateio. O excedente retorna ao sistema como fluxo único para o fomento. A associação territorial da empresa não participa do cálculo no MVP. O valor é calculado no momento da simulação e não é recalculado. O cruzamento com o território entra na E-4 (Fase 2). Para recálculo de simulações existentes com novos parâmetros, o handler de parâmetros.atualizados (Fase 2) será responsável.


4.1 AlgoritmoExcedenteService.calcular(): algoritmo principal

Seção intitulada “4.1 AlgoritmoExcedenteService.calcular(): algoritmo principal”
função calcular(receitaBruta: Decimal, custosOperacionais: Decimal,
folhaReequilibrada: Decimal, qtdTrabalhadores: number,
params: DivisaoParams) -> SimulacaoResultado:
entrada:
receitaBruta: receita bruta declarada (>= 0)
custosOperacionais: custos operacionais declarados (>= 0, excluindo folha)
folhaReequilibrada: custo total da folha após reequilíbrio (>= 0)
qtdTrabalhadores: total de colaboradores (>= 1)
params: { percentualFomento, percentualReinvestimento, percentualTrabalhadores }
// 1. Calcular excedente
excedente = receitaBruta.minus(custosOperacionais).minus(folhaReequilibrada)
// 2. Se excedente <= 0, todos os valores de distribuição são zero
se excedente.lte(0):
retornar {
excedenteCalculado: excedente.toDecimalPlaces(2),
valorFomentoTotal: new Decimal(0),
valorReinvestimento: new Decimal(0),
valorTrabalhadoresTotal: new Decimal(0),
valorPorTrabalhador: new Decimal(0),
percentualFomento: params.percentualFomento,
percentualReinvestimento: params.percentualReinvestimento,
percentualTrabalhadores: params.percentualTrabalhadores,
}
// 3. Aplicar divisão parametrizada
valorFomentoTotal = excedente.mul(params.percentualFomento).div(100).toDecimalPlaces(2)
valorReinvestimento = excedente.mul(params.percentualReinvestimento).div(100).toDecimalPlaces(2)
valorTrabalhadoresTotal = excedente.mul(params.percentualTrabalhadores).div(100).toDecimalPlaces(2)
// 4. Calcular valor por trabalhador
se qtdTrabalhadores > 0:
valorPorTrabalhador = valorTrabalhadoresTotal.div(qtdTrabalhadores).toDecimalPlaces(2)
senão:
valorPorTrabalhador = new Decimal(0)
// 5. Retornar com o excedente arredondado
retornar {
excedenteCalculado: excedente.toDecimalPlaces(2),
valorFomentoTotal,
valorReinvestimento,
valorTrabalhadoresTotal,
valorPorTrabalhador,
percentualFomento: params.percentualFomento,
percentualReinvestimento: params.percentualReinvestimento,
percentualTrabalhadores: params.percentualTrabalhadores,
}

Entrada: receita = 1.000.000,00, custos = 400.000,00, folha_reequilibrada = 350.000,00, qtd_trabalhadores = 25, params = {40, 40, 20}

Etapa Valor
Excedente 1.000.000 - 400.000 - 350.000 = 250.000,00
Valor fomento (40%) 250.000 × 0,40 = 100.000,00
Valor caixa interno (40%) 250.000 × 0,40 = 100.000,00
Valor trabalhadores total (20%) 250.000 × 0,20 = 50.000,00
Valor por trabalhador 50.000 / 25 = 2.000,00

Se excedente <= 0: todos os valor_* são 0,00. A simulação é publicada mesmo assim e demonstra que a empresa não gera excedente sob os parâmetros atuais.

4.3 SimulacaoService.onBalancoSubmetido(): fluxo real

Seção intitulada “4.3 SimulacaoService.onBalancoSubmetido(): fluxo real”
função onBalancoSubmetido(evento: EventoConsultado):
sequencia = BigInt(evento.sequence_number)
// 1. Idempotência
simulacaoExistente = simulacaoRepo.buscarPorBalancoEventoId(evento.event_id)
se simulacaoExistente:
logger.log("Simulação já existe para este balanço — ignorando")
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
pendenteExistente = pendenteRepo.buscarPorEventoId(evento.event_id)
se pendenteExistente:
logger.log("Balanço já está pendente — ignorando")
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
// 2. Extração e validação do payload
dados = extrairDadosBalanco(evento)
se dados é nulo: // payload incompleto, período inválido
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
se receita < 0 ou custos < 0:
logger.error("Receita ou custos negativos — balanço descartado")
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
// 3. Empresa
empresa = empresaRepo.buscarPorId(dados.empresaId)
se empresa é nula:
logger.warn("Empresa não está na projeção — balanço pendente")
armazenarPendente(dados, sequencia)
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
se empresa.status NOT IN ('cadastrada', 'socializada'):
logger.log("Empresa inativa para simulação — balanço ignorado")
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
// 4. Folha e diagnóstico
folha = folhaRepo.buscarPorId(dados.referenciaSubmissaoFolha)
se folha é nula, custo_total_depois é nulo ou total_colaboradores <= 0:
logger.log("Balanço pendente — aguardando folha, diagnóstico ou total")
armazenarPendente(dados, sequencia)
offsetRepo.upsert('empresa.balanço_submetido', sequencia)
retornar
// 5. Processar
processarBalanco(dados)

armazenarPendente conta os pendentes da empresa. Ao atingir o limite de 10, loga o descarte sem inserir.

4.4 SimulacaoService.processarBalanco(): fluxo real

Seção intitulada “4.4 SimulacaoService.processarBalanco(): fluxo real”
função processarBalanco(dados: BalancoDados):
// 1. Reverificar a folha
folha = folhaRepo.buscarPorId(dados.referenciaSubmissaoFolha)
se folha é nula, custo_total_depois é nulo,
diagnostico_id é nulo ou total_colaboradores <= 0:
logger.warn("Folha referenciada ainda incompleta — balanço não processado")
retornar // o pendente permanece na tabela
// 2. Executar o algoritmo
resultado = algoritmo.calcular(
dados.receitaBruta, dados.custosOperacionais,
new Decimal(folha.custo_total_depois), folha.total_colaboradores,
parametros.getDivisaoParams(),
)
// 3. Construir o payload
payload = {
empresa_id: dados.empresaId,
submissao_id: dados.submissaoId,
receita_declarada, custos_declarados, folha_reequilibrada,
excedente_calculado, total_colaboradores,
divisao: { percentual_fomento, percentual_reinvestimento,
percentual_trabalhadores, valor_fomento_total,
valor_reinvestimento, valor_trabalhadores_total,
valor_por_trabalhador },
referencia_submissao_folha, diagnostico_id,
periodo_referencia: { inicio, fim },
versao_parametros: parametros.getVersao(),
}
// 4. Persistir
simulacaoId = UUID v4
inserido = simulacaoRepo.inserir({ id: simulacaoId, balanco_evento_id, ..., payload_simulacao: payload })
se !inserido: // UNIQUE por balanco_evento_id ou balanco_submissao_id
garantirPublicacaoSimulacao(dados.eventoId)
se dados.pendenteId: pendenteRepo.remover(dados.pendenteId)
avancarOffsetSeHouver(dados)
retornar
// 5. Publicar com retry e marcar o evento publicado
publicarComRetry(eventBus, {
tipo: 'empresa.simulação_econômica_publicada',
origem: 'E-3',
event_id: simulacaoId,
correlacao_id: dados.submissaoId,
versao_schema: '2.0.0',
payload,
})
simulacaoRepo.atualizarEventoPublicadoEm(simulacaoId, agora)
// 6. Remover o pendente e avançar o cursor
se dados.pendenteId: pendenteRepo.remover(dados.pendenteId)
avancarOffsetSeHouver(dados)

garantirPublicacaoSimulacao busca a simulação existente e a republica apenas quando evento_publicado_em está nulo. A falha definitiva de publicarComRetry propaga a exceção. O cursor não avança, o evento de entrada vai para a DLQ e a simulação órfã é republicada pela varredura do boot.

4.5 Protocolo de inicialização: SimulacaoService.iniciar()

Seção intitulada “4.5 Protocolo de inicialização: SimulacaoService.iniciar()”
função iniciar():
se iniciado: retornar
iniciado = true
// 1. Seed do cursor com a maior sequência do barramento
maiorSequence = await eventBus.obterMaiorSequence()
await offsetRepo.seed(['empresa.balanço_submetido'], maiorSequence)
// 2. Replay dos eventos perdidos
await reprocessarEventosPerdidos()
// 3. Republicar simulações sem evento publicado
await republicarSimulacoesOrfas()
// 4. Varredura de pendentes
await varrerPendentes()
// 5. Registrar o consumidor para eventos futuros
registrarConsumidores()
// 6. Timer da varredura periódica (60 segundos)
iniciarTimerVarrimento()
logger.log("E-3 inicializada — handler de empresa.balanço_submetido")

reprocessarEventosPerdidos lê o cursor de e3.consumer_offset e chama eventBus.replayDeSequence(cursor, ['empresa.balanço_submetido']), despachando cada evento pelo handler. republicarSimulacoesOrfas busca até 100 simulações com evento_publicado_em nulo e as republica. varrerPendentes busca até 200 pendentes e processa os que têm empresa ativa e folha completa. Os serviços de projeção seguem o mesmo protocolo para os seus tipos, com seed e replay próprios.

O algoritmo usa Decimal (via biblioteca decimal.js) para todas as operações monetárias. O tipo number do JavaScript (IEEE 754 double) não é adequado para valores financeiros. O uso de Decimal garante:

  • Divisão dos percentuais sem erro acumulado de ponto flutuante
  • valor_fomento_total + valor_reinvestimento + valor_trabalhadores_total ≈ excedente ao centavo
import Decimal from 'decimal.js';
// Divisão sem rateio: cada destino recebe sua fatia diretamente do excedente.
// Não há função de rateio entre UCs — o retorno ao sistema é um fluxo único.
Caso Comportamento
excedente_calculado <= 0 Simulação publicada com todos os valor_* = 0. A informação “não há excedente” é pública e relevante para comparação entre empresas do mesmo porte e setor.
empresa_id não está na projeção Balanço armazenado como pendente. Quando empresa.cadastrada chegar, processarPendentesDaEmpresa reprocessa.
referencia_submissao_folha não está na projeção Balanço armazenado como pendente. Quando empresa.folha_submetida chegar, processarPendentesDaFolha reprocessa.
custo_total_depois IS NULL na projeção Balanço armazenado como pendente. Dois caminhos de desbloqueio: empresa.diagnóstico_salarial_publicado chega e o handler verifica os pendentes, ou a varredura periódica encontra a folha completa.
total_colaboradores = 0 na projeção O balanço permanece pendente. O processamento só ocorre quando a submissão da folha traz o total maior ou igual a 1.
Payload de projeção incompleto ou com valor fora da faixa Descarte com log e cursor avançado. O evento não trava a fila.

5. Integração com o Barramento e Outras Colônias

Seção intitulada “5. Integração com o Barramento e Outras Colônias”

A E-3 injeta EventBusService (do módulo @Global() N-0a) e usa o Logger do NestJS. Não há injeção de serviços da N-0c; os logs são emitidos pelo Logger padrão da aplicação.

Operação Chamada Tipos de evento
Publicar balanço publicarComRetry(eventBus, {...}) empresa.balanço_submetido
Publicar simulação publicarComRetry(eventBus, {...}) empresa.simulação_econômica_publicada
Inscrever eventBus.inscrever(tipo, 'E-3', handler) Os quatro tipos consumidos e o stub parâmetros.atualizados
Replay eventBus.replayDeSequence(cursor, [tipo]) Os quatro tipos com cursor, nos respectivos serviços
Maior sequência eventBus.obterMaiorSequence() Seed dos cursores no boot

O EventBusService é injetável sem import explícito porque EventBusModule é @Global().

A E-3 expõe duas rotas com o prefixo global /api:

Rota Autenticação Throttle Respostas
POST /api/empresas/:id/balanco AuthGuard JWT; o sub precisa ser o representante_id da empresa 10/hora 202 aceito; 400 validação; 401 token ausente ou inválido; 403 representante diferente; 404 empresa fora da projeção; 409 empresa inativa ou balanço duplicado; 429 limite; 500 falha de publicação
GET /api/empresas/:id/simulacoes pública 60/minuto 200 com a página do histórico (lista vazia quando não há simulação); 400 paginação inválida; 429 limite

A listagem devolve page padrão 1 e limit padrão 20, máximo 50, com os itens da mais recente para a mais antiga. A resposta espelha o payload público do evento, sem payload_simulacao nem campos internos, e acrescenta simulacao_id e calculado_em.

A E-3 não faz chamadas HTTP a outras colônias. A comunicação é via barramento.

5.3 Dependências de projeções de leitura de outras colônias

Seção intitulada “5.3 Dependências de projeções de leitura de outras colônias”

A E-3 mantém projeções locais derivadas de eventos de outras colônias. Não há dependência de query direta a bancos de outras colônias:

Projeção local Origem do dado Colônia de origem Campos extraídos
e3.empresas_projecao empresa.cadastrada E-1 empresa_id, razao_social, porte, setor_id, status, representante_id
e3.folhas_projecao empresa.folha_submetida + empresa.diagnóstico_salarial_publicado E-2 submissao_id, empresa_id, total_colaboradores, custo_total_depois, versao_parametros, diagnostico_id

A E-3 consome um evento da E-1:

  • empresa.cadastrada — mantém a projeção local de empresas ativas, com o status e o representante.

A E-3 não consome empresa.associação_territorial_definida. O rateio por UC saiu do MVP e o excedente retorna ao sistema como fluxo único para o fomento. O evento de associação territorial continua sendo produzido pela E-1 e permanece disponível para a E-4 (Fase 2).

A E-1 não sabe que a E-3 existe. Publica seus eventos e o contrato está cumprido.

A E-3 consome dois eventos da E-2:

  • empresa.folha_submetidatotal_colaboradores
  • empresa.diagnóstico_salarial_publicadocusto_total_depois

O total_colaboradores não está no payload do diagnóstico, que só contém colaboradores_acima_teto e colaboradores_abaixo_minimo, os afetados pela redistribuição. Por isso a E-3 também consome empresa.folha_submetida, que contém o total declarado. A tabela e3.folhas_projecao unifica os dois eventos em uma linha. A submissão cria a linha e o diagnóstico a atualiza.

A E-2 não sabe que a E-3 existe. Publica o diagnóstico e a submissão de folha, e seus contratos estão cumpridos.

A leitura pública das simulações é servida pela própria E-3, no GET /api/empresas/:id/simulacoes, consumido pela seção de simulação da página de empresa no web. A D-7 não consome empresa.simulação_econômica_publicada e não projeta os dados da E-3. O padrão de leitura em e3.simulacoes é a listagem por empresa, ordenada por criado_em decrescente.

A E-4 consome empresa.simulação_econômica_publicada e ranking.atualizado. A associação territorial da empresa, produzida pela E-1, é o insumo que faltava a esta colônia para cruzar o retorno ao sistema com as demandas do território. O quantidade_ucs_empresa do payload de associação territorial segue disponível para esse consumo.


As rotas da face BFF aplicam throttle por IP com @nestjs/throttler:

Rota Limite Janela
POST /api/empresas/:id/balanco 10 1 hora
GET /api/empresas/:id/simulacoes 60 1 minuto

Limites aplicados pela E-3 no processamento:

Constante Valor Descrição
E3_MAX_BALANCOS_PENDENTES_POR_EMPRESA 10 (env) Máximo de balanços pendentes simultâneos por empresa. Acima disso, log.warn e descarte.
E3_MAX_VALOR_MONETARIO 9.999.999.999.999,99 Teto de receita e custos aceitos na submissão.
E3_LISTAGEM_LIMITE_PADRAO / E3_LISTAGEM_LIMITE_MAXIMO 20 / 50 Paginação da leitura pública de simulações.
LIMITE_VARREDURA_PENDENTES 200 Pendentes processados por rodada de varredura.
LIMITE_VARREDURA_ORFAOS 100 Simulações e submissões órfãs republicadas por varredura.
E3_INTERVALO_VARREDURA_PENDENTES_MS 60.000 Intervalo da varredura periódica de pendentes.
Query Frequência Índice
“Folha por submissao_id” (lookup para processar balanço) Toda simulação PK de e3.folhas_projecao (submissao_id)
“Balanços pendentes para esta folha” (quando a folha ou o diagnóstico chega) A cada diagnóstico balancos_pendentes_referencia_submissao_folha_idx
“Balanços pendentes desta empresa” (quando empresa.cadastrada chega) A cada empresa balancos_pendentes_empresa_id_idx
“Simulações da empresa X, mais recentes primeiro” (leitura pública) Leitura via GET /simulacoes simulacoes_empresa_id_criado_em_idx
“Simulação já existe para este balanco_evento_id?” Idempotência na chegada do evento simulacoes_balanco_evento_id_key (UNIQUE)
“Simulação já existe para este balanco_submissao_id?” Idempotência alternativa simulacoes_balanco_submissao_id_key (UNIQUE)
“Submissões sem evento publicado” (varredura do boot) Boot balancos_submissoes_evento_publicado_em_pendente_idx (parcial); simulacoes_evento_publicado_em_pendente_idx (parcial)
“Hash de idempotência já existe?” Toda submissão balancos_submissoes_idempotencia_hash_key (UNIQUE)

Os parâmetros de divisão do excedente (DivisaoParams) são constantes de config/parametros.config.ts, carregadas com o módulo e devolvidas por ParametrosService.getDivisaoParams(). No MVP, os parâmetros não mudam; não há invalidação nem TTL.

Na Fase 2, quando parâmetros.atualizados passar a ser consumido, o ParametrosService atualiza os parâmetros em memória no handler do evento. Redis não se justifica para este volume.

Não há cache para as projeções (e3.empresas_projecao, e3.folhas_projecao). O volume esperado no MVP (dezenas a centenas de empresas, cada uma com menos de 5 folhas) não justifica cache em aplicação. As consultas são por chave primária e cobertas por índices.

O algoritmo de excedente é O(1): operações aritméticas sobre valores escalares com decimal.js, sem chamada externa. O gargalo potencial está no desbloqueio de balanços pendentes: quando um diagnóstico chega, o handler consulta e3.balancos_pendentes por referencia_submissao_folha. Com o índice dedicado, a query é O(log n). Para múltiplos balanços pendentes, cada um dispara um processamento sequencial.


Os specs instanciam o serviço testado em um TestingModule do NestJS, com o AlgoritmoExcedenteService real e os repositórios, o EventBusService e o ParametrosService substituídos por mocks:

const module: TestingModule = await Test.createTestingModule({
providers: [
SimulacaoService,
AlgoritmoExcedenteService,
{ provide: ParametrosService, useValue: mockParametros },
{ provide: EventBusService, useValue: mockEventBus },
{ provide: EmpresaProjecaoRepository, useValue: mockEmpresaRepo },
{ provide: FolhaDiagnosticoProjecaoRepository, useValue: mockFolhaRepo },
{ provide: BalancoPendenteRepository, useValue: mockPendenteRepo },
{ provide: SimulacaoRepository, useValue: mockSimulacaoRepo },
{ provide: ConsumerOffsetRepository, useValue: mockOffsetRepo },
],
}).compile();

Os specs existentes cobrem SimulacaoService, BalancoService, ConsultaSimulacoesService, ParametrosService, ProjecaoEmpresasService, ProjecaoFolhaService e AlgoritmoExcedenteService, além de um spec de schema e um do guard.

Happy path:

ID Cenário Verificação
T1 Balanço chega com folha e diagnóstico disponíveis empresa.simulação_econômica_publicada publicado. e3.simulacoes com 1 linha.
T2 Excedente positivo divisao com os três destinos calculados e valor_por_trabalhador == valor_trabalhadores_total / total_colaboradores.
T3 Excedente zero (receita = custos + folha) Simulação publicada com todos os valor_* = 0.

Bordas e resiliência:

ID Cenário Verificação
T4 Balanço chega antes do diagnóstico da folha Balanço vai para e3.balancos_pendentes. Nenhum evento publicado. Quando o diagnóstico chega, a simulação é processada.
T5 Balanço chega antes de empresa.folha_submetida Balanço pendente. Quando a folha chega, os pendentes são verificados.
T6 Diagnóstico chega antes de empresa.folha_submetida e3.folhas_projecao recebe INSERT com total_colaboradores = 0. Quando a folha chega, o UPDATE preenche o total e verifica os pendentes.
T7 empresa.balanço_submetido duplicado (mesmo event_id) Segunda chegada encontra a simulação existente. Log e retorno sem efeito.
T8 empresa.balanço_submetido duplicado (event_id diferente, mesmo submissao_id) UNIQUE no INSERT. A simulação existente é republicada quando evento_publicado_em está nulo; o pendente é removido.
T9 Empresa não está na projeção Balanço pendente. Quando empresa.cadastrada chega, processarPendentesDaEmpresa reprocessa.
T10 Empresa com status = 'recusada' Balanço ignorado. Log. Sem efeito colateral.
T11 parâmetros.atualizados chega no MVP Handler stub: log. Sem efeito colateral.
T12 Publicação do evento de simulação falha Simulação persistida com evento_publicado_em = NULL. Cursor não avança. Republicação no boot.
T13 Payload de balanço incompleto, com período inválido ou valores negativos Descarte com log. Cursor avança.
T14 Limite de pendentes por empresa atingido Descarte com log.warn. Cursor avança.
T15 Varredura periódica encontra pendente com folha completa Pendente processado e removido sem evento novo.

O AlgoritmoExcedenteService é testável com zero mocks:

describe('AlgoritmoExcedenteService', () => {
let service: AlgoritmoExcedenteService;
beforeEach(() => {
service = new AlgoritmoExcedenteService();
});
it('deve calcular excedente positivo com divisão 40/40/20', () => {
const resultado = service.calcular(
new Decimal(1_000_000),
new Decimal(400_000),
new Decimal(350_000),
25, // total_colaboradores
{ percentualFomento: 40, percentualReinvestimento: 40, percentualTrabalhadores: 20 },
);
expect(resultado.excedenteCalculado.toNumber()).toBe(250_000);
expect(resultado.valorFomentoTotal.toNumber()).toBe(100_000);
expect(resultado.valorReinvestimento.toNumber()).toBe(100_000);
expect(resultado.valorTrabalhadoresTotal.toNumber()).toBe(50_000);
expect(resultado.valorPorTrabalhador.toNumber()).toBe(2_000);
});
it('deve retornar zeros quando excedente é negativo', () => {
const resultado = service.calcular(
new Decimal(100_000),
new Decimal(200_000),
new Decimal(50_000),
10,
{ percentualFomento: 40, percentualReinvestimento: 40, percentualTrabalhadores: 20 },
);
expect(resultado.excedenteCalculado.toNumber()).toBe(-150_000);
expect(resultado.valorFomentoTotal.toNumber()).toBe(0);
expect(resultado.valorReinvestimento.toNumber()).toBe(0);
expect(resultado.valorTrabalhadoresTotal.toNumber()).toBe(0);
expect(resultado.valorPorTrabalhador.toNumber()).toBe(0);
});
it('deve tratar zero trabalhadores', () => {
const resultado = service.calcular(
new Decimal(100_000), new Decimal(0), new Decimal(0),
0,
{ percentualFomento: 40, percentualReinvestimento: 40, percentualTrabalhadores: 20 },
);
expect(resultado.valorPorTrabalhador.toNumber()).toBe(0);
});
});

Os testes do módulo também cobrem excedente zero, percentuais parametrizados e arredondamento de cada destino em duas casas decimais.

O prisma/seed-dev.ts não popula as tabelas da E-3. Para exercitar o fluxo no ambiente local, as projeções podem ser inseridas direto no banco e a submissão feita pelo endpoint:

-- Projeção de empresas, populada por replay em produção
INSERT INTO e3.empresas_projecao (empresa_id, razao_social, porte, setor_id, representante_id, status) VALUES
('e1000001-0000-0000-0000-000000000001', 'TechBrasil Ltda', 'media', 'tecnologia', '<cidadao_id>', 'cadastrada'),
('e1000001-0000-0000-0000-000000000002', 'Padaria Pão Quente', 'micro', 'alimentacao', '<cidadao_id>', 'socializada'),
('e1000001-0000-0000-0000-000000000003', 'Construtora Exemplo S.A.', 'grande', 'construcao', '<cidadao_id>', 'cadastrada');
-- Projeção de folha (submissão + diagnóstico)
INSERT INTO e3.folhas_projecao (submissao_id, empresa_id, total_colaboradores, custo_total_depois, diagnostico_id, diagnostico_publicado_em, versao_parametros_diag) VALUES
('f1000001-0000-0000-0000-000000000001', 'e1000001-0000-0000-0000-000000000001', 25, 350000.00, 'd1000001-0000-0000-0000-000000000001', NOW() - INTERVAL '1 day', 'mvp-v1'),
('f1000002-0000-0000-0000-000000000002', 'e1000001-0000-0000-0000-000000000002', 8, 42000.00, 'd1000002-0000-0000-0000-000000000002', NOW() - INTERVAL '2 days', 'mvp-v1'),
('f1000003-0000-0000-0000-000000000003', 'e1000001-0000-0000-0000-000000000003', 120, 1800000.00, 'd1000003-0000-0000-0000-000000000003', NOW() - INTERVAL '3 days', 'mvp-v1');

O representante_id precisa ser o cidadao_id de um JWT de desenvolvimento. Os cursores de e3.consumer_offset são semeados no boot com obterMaiorSequence(), sem seed manual.

Janela do terminal
# Token de desenvolvimento para o representante (usa JWT_SECRET local)
npm run token:dev -- <cidadao_id>
# Submeter um balanço (202 Accepted)
curl -X POST http://localhost:3000/api/empresas/<empresa_id>/balanco \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"receita_bruta": 1000000,
"custos_operacionais": 400000,
"referencia_submissao_folha": "f1000001-0000-0000-0000-000000000001",
"periodo_referencia": { "inicio": "2026-01-01", "fim": "2026-12-31" }
}'
# Verificar a simulação gerada
psql -c "SELECT id, empresa_id, excedente_calculado, valor_fomento_total, evento_publicado_em FROM e3.simulacoes ORDER BY criado_em DESC LIMIT 1;"

Uma segunda submissão com o mesmo hash de conteúdo responde 409 com o submissao_id existente. A leitura pública do resultado usa o GET /api/empresas/:id/simulacoes.


Item Status
Consumo de empresa.cadastrada com projeção local de empresas, status e representante MVP obrigatório
Consumo de empresa.folha_submetida para total_colaboradores MVP obrigatório
Consumo de empresa.diagnóstico_salarial_publicado para custo_total_depois MVP obrigatório
Consumo de empresa.balanço_submetido como gatilho de processamento MVP obrigatório
Face BFF: POST /api/empresas/:id/balanco com JWT, hash de idempotência e 202 MVP obrigatório
Leitura pública: GET /api/empresas/:id/simulacoes com paginação MVP obrigatório
Algoritmo de cálculo de excedente com decimal.js MVP obrigatório
Divisão 40/40/20 parametrizada (config estática) com retorno ao sistema MVP obrigatório
Tabela de balanços pendentes com desbloqueio por evento e varredura no boot e a cada 60 segundos MVP obrigatório
Publicação de empresa.simulação_econômica_publicada com payload completo (versão 2.0.0) MVP obrigatório
Idempotência por event_id e submissao_id MVP obrigatório
Replay de eventos na inicialização via replayDeSequence() MVP obrigatório
Republicação de simulações e submissões de balanço órfãs no boot MVP obrigatório
Logs com empresa_id, submissao_id e simulacao_id para rastreabilidade MVP obrigatório
Dado bruto do balanço preservado no payload da simulação MVP obrigatório
Simplificação Justificativa Migração Fase 2
Face BFF embutida na própria E-3 O formulário de balanço tem quatro campos. Um módulo BFF externo não se justifica. Se o balanço ganhar novas fontes, avaliar BFF dedicado ou extensão da D-1a.
Parâmetros de divisão fixos em config/parametros.config.ts ({ fomento: 40, reinvestimento: 40, trabalhadores: 20 }) Sem sistema de parametrização dinâmica em produção no MVP. Os valores de referência são o ponto de partida a calibrar. Migrar para consumo do evento parâmetros.atualizados da governança de parâmetros na Fase 3, com recálculo em lote de simulações existentes.
Handler stub de parâmetros.atualizados (log e ignora) O evento não é publicado por nenhuma colônia no MVP. O handler existe para contrato definido. Implementar onParametrosAtualizados() com recálculo de simulações existentes.
Retorno ao sistema sem rateio por UC O excedente retorna como fluxo único para o fomento. A associação territorial da empresa não participa do cálculo no MVP. A E-4 cruza o retorno ao sistema com as demandas do ranking das UCs do território da empresa.
Sem verificação cruzada com bases externas (Receita Federal, SPED) A declaração é pública e verificável socialmente. O sistema de transparência radical é o mecanismo de pressão por veracidade. Adicionar validação cruzada como colônia dedicada ou enriquecimento da E-3.
Sem recálculo de simulações existentes por mudança de parâmetros Cada simulação é um snapshot dos parâmetros no momento do cálculo. Recálculo em lote por mudança de parâmetros é complexidade desproporcional para o MVP. Implementar recálculo seletivo no handler de parâmetros.atualizados ou via scheduled job.
Sem projeção de empresa.atualizada (atualização cadastral) A E-1 publica empresa.cadastrada com UPSERT no consumer, o que cobre atualizações. Um evento separado não é necessário para a E-3 no MVP. Se a E-1 passar a publicar empresa.atualizada, adicionar handler correspondente.
Item Motivo
Consumo de parâmetros.atualizados com recálculo de simulações existentes O sistema de parametrização dinâmica entra na Fase 3. O handler stub está pronto para ser ativado.
E-4: cruzamento do retorno ao sistema com o território O impacto territorial entra na Fase 2. O volume de operação territorial não é um dado capturado no MVP.
Recálculo automático de simulações por mudança de parâmetros Complexidade desproporcional. Cada simulação é um snapshot do momento.
Verificação cruzada com bases externas (Receita Federal, SPED, CAGED) Fora do escopo do MVP. O mecanismo de pressão por veracidade é a transparência radical.
Queries de contenção e sobreposição do período contábil (DATERANGE) O Prisma não suporta o tipo. O período vive em duas colunas DATE; as queries ficam para quando houver caso de uso.
Exportação de dados agregados de simulação (dataset público, CSV) Infraestrutura de exportação entra na Fase 2 (Pesquisa e Exportação).
Validação de porte cruzada com faturamento declarado Consistência entre porte autodeclarado e receita bruta é verificação de qualidade de dado, não crítica para o MVP.

Documento Seção relevante
contexto_IA.md Seção 15 (Relação com empresas), Seção 22 (Arquitetura técnica — O Formigueiro, colônias de empresa)
rede_civica.md Parte II (A Empresa, divisão do excedente), Apêndice Técnico, Apêndice B — Colônias, Grupo Empresa, Fase 1 (MVP), e Apêndice C — Parâmetros (divisão do excedente e limites da E-3)
Apêndice B - Colônias.md Seção “Colônia E-3 — Simulação Econômica” e “Mapa de Dependências de Eventos entre Colônias”
N-0b - Registry.md Schemas empresa.cadastrada, empresa.folha_submetida, empresa.diagnóstico_salarial_publicado, empresa.balanço_submetido, empresa.simulação_econômica_publicada e parâmetros.atualizados
E-1 - Cadastro Institucional.md Seção 5.4 (Relação com a E-2 e E-3)
E-2 - Transparência Salarial e Folha.md Seção 5.4 (Relação com a E-3)
N-0a - Event Bus.md API do EventBusService: publicar, inscrever, replayDeSequence e obterMaiorSequence