E-3 — Simulação Econômica
Parte das Colônias de Empresas — Fase 1
Propósito
Seção intitulada “Propósito”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).
Referência
Seção intitulada “Referência”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.
1. Estrutura do Módulo NestJS
Seção intitulada “1. Estrutura do Módulo NestJS”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.
1.1 Árvore de diretórios
Seção intitulada “1.1 Árvore de diretórios”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.
1.2 Module definition
Seção intitulada “1.2 Module definition”@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(); }}1.3 Pontos de atenção
Seção intitulada “1.3 Pontos de atenção”- 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
EventBusModuleexplicitamente.EventBusModuleé@Global(), e oEventBusServiceé 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
OnModuleInitinicializa 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
AuthGuardda face BFF valida o JWT comjsonwebtokene oJWT_SECRETcompartilhado com a D-1a. Osubdo token é orepresentante_idda empresa.
1.4 Serviços — responsabilidades e contratos
Seção intitulada “1.4 Serviços — responsabilidades e contratos”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.
1.5 Projeções locais e face BFF
Seção intitulada “1.5 Projeções locais e face BFF”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.
2. Banco de Dados — Schema e Tabelas
Seção intitulada “2. Banco de Dados — Schema e Tabelas”2.1 Schema e3
Seção intitulada “2.1 Schema e3”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.
2.2 Tabela e3.empresas_projecao
Seção intitulada “2.2 Tabela e3.empresas_projecao”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.
Colunas — detalhamento
Seção intitulada “Colunas — detalhamento”| 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. |
2.3 Tabela e3.folhas_projecao
Seção intitulada “2.3 Tabela e3.folhas_projecao”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.
Colunas — detalhamento
Seção intitulada “Colunas — detalhamento”| 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). |
2.4 Tabela e3.balancos_submissoes
Seção intitulada “2.4 Tabela e3.balancos_submissoes”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.
Colunas — detalhamento
Seção intitulada “Colunas — detalhamento”| 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. |
2.5 Tabela e3.balancos_pendentes
Seção intitulada “2.5 Tabela e3.balancos_pendentes”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.
Colunas — detalhamento
Seção intitulada “Colunas — detalhamento”| 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. |
2.6 Tabela e3.simulacoes
Seção intitulada “2.6 Tabela e3.simulacoes”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.
Colunas — detalhamento
Seção intitulada “Colunas — detalhamento”| 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. |
2.7 Tabela e3.consumer_offset
Seção intitulada “2.7 Tabela e3.consumer_offset”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.
2.8 Migrations
Seção intitulada “2.8 Migrations”Cinco migrations formam o schema e3:
20260813193220_create_e3_tables— cria o schemae3, as tabelasempresas_projecao,folhas_projecao,balancos_submissoes,balancos_pendentes,simulacoeseconsumer_offset, com os índices. Na criação,simulacoesainda tinha as colunas do rateio por UC (quantidade_ucs,percentual_uc,destinos_uc) e existia a tabelaempresa_ucs.20260818090000_add_representante_id_empresas_projecao— adicionarepresentante_ide o índice nas projeções de empresa da E-2 e da E-3.20260818120000_backfill_representante_id_empresas_projecao— preencherepresentante_iddas projeções a partir do payload deempresa.cadastradanocore.event_log. Registros sem evento correspondente permanecem nulos e bloqueados para submissão de balanço.20260820170000_e3_excedente_fomento— renomeiapercentual_ucparapercentual_fomentoevalor_uc_totalparavalor_fomento_total, removequantidade_ucsedestinos_ucdesimulacoese dropaempresa_ucs. O excedente deixa de ser rateado por UC e retorna ao sistema como fluxo único.20260918180000_e3_varredura_orfaos_indices— cria os índices parciais deevento_publicado_empara as varreduras de órfãos debalancos_submissoesesimulacoes.
As projeções não têm seed por migration. São populadas pelo replay dos eventos no boot.
2.9 Relações internas
Seção intitulada “2.9 Relações internas”O schema e3 não tem foreign keys internas. Todas as tabelas são projeções independentes:
e3.empresas_projecao— alimentada porempresa.cadastradae3.folhas_projecao— alimentada porempresa.folha_submetida+empresa.diagnóstico_salarial_publicadoe3.balancos_submissoes— alimentada pela face BFFe3.balancos_pendentes— alimentada porempresa.balanço_submetidoquando o processamento é bloqueadoe3.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.
2.10 Decisões de schema
Seção intitulada “2.10 Decisões de schema”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.
3. Eventos — Contratos Detalhados
Seção intitulada “3. Eventos — Contratos Detalhados”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.
3.3 Evento consumido: empresa.cadastrada
Seção intitulada “3.3 Evento consumido: empresa.cadastrada”| 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.
3.4 Evento consumido: empresa.folha_submetida
Seção intitulada “3.4 Evento consumido: empresa.folha_submetida”| 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 pendentes3.9 Ordem de operações: consumir projeções
Seção intitulada “3.9 Ordem de operações: consumir projeções”empresa.cadastrada
Seção intitulada “empresa.cadastrada”1. Extrair dados: empresa_id, razao_social, porte, setor_id, status, representante_id2. Payload incompleto ou status inválido: log.error, avançar cursor, retornar3. UPSERT em e3.empresas_projecao por empresa_id4. processarPendentesDaEmpresa(empresa_id)5. Avançar cursorempresa.folha_submetida
Seção intitulada “empresa.folha_submetida”1. Extrair dados: submissao_id, empresa_id, total_colaboradores2. Payload incompleto ou total < 1: log.error, avançar cursor, retornar3. UPSERT em e3.folhas_projecao por submissao_id4. processarPendentesDaFolha(submissao_id)5. Avançar cursorempresa.diagnóstico_salarial_publicado
Seção intitulada “empresa.diagnóstico_salarial_publicado”1. Extrair dados: submissao_id, empresa_id, custo_total_depois, versao_parametros2. Payload incompleto ou custo negativo: log.error, avançar cursor, retornar3. UPSERT do diagnóstico em e3.folhas_projecao (UPDATE e INSERT parcial)4. processarPendentesDaFolha(submissao_id)5. Avançar cursor3.10 Idempotência
Seção intitulada “3.10 Idempotência”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.
3.11 Tratamento de erro e reentrega
Seção intitulada “3.11 Tratamento de erro e reentrega”| 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. |
3.12 Decisões de design com justificativa
Seção intitulada “3.12 Decisões de design com justificativa”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. Lógica de Negócio — Algoritmos e Fluxos
Seção intitulada “4. Lógica de Negócio — Algoritmos e Fluxos”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, }4.2 Demonstração numérica
Seção intitulada “4.2 Demonstração numérica”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.
4.6 Aritmética decimal
Seção intitulada “4.6 Aritmética decimal”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 ≈ excedenteao 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.4.7 Casos de borda — tratamento detalhado
Seção intitulada “4.7 Casos de borda — tratamento detalhado”| 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”5.1 Publicação e consumo no barramento
Seção intitulada “5.1 Publicação e consumo no barramento”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().
5.2 Face BFF e rotas de leitura
Seção intitulada “5.2 Face BFF e rotas de leitura”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 |
5.4 Relação com a E-1
Seção intitulada “5.4 Relação com a E-1”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.
5.5 Relação com a E-2
Seção intitulada “5.5 Relação com a E-2”A E-3 consome dois eventos da E-2:
empresa.folha_submetida—total_colaboradoresempresa.diagnóstico_salarial_publicado—custo_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.
5.6 Leitura pública e a D-7
Seção intitulada “5.6 Leitura pública e a D-7”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.
5.7 Relação com a E-4 (Fase 2)
Seção intitulada “5.7 Relação com a E-4 (Fase 2)”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.
6. Performance e Limites
Seção intitulada “6. Performance e Limites”6.1 Rate limiting e cotas
Seção intitulada “6.1 Rate limiting e cotas”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. |
6.2 Índices e padrões de query
Seção intitulada “6.2 Índices e padrões de query”| 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) |
6.3 Estratégia de cache
Seção intitulada “6.3 Estratégia de cache”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.
6.4 Complexidade do algoritmo
Seção intitulada “6.4 Complexidade do algoritmo”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.
7. Testabilidade
Seção intitulada “7. Testabilidade”7.1 Testes de serviço com mocks
Seção intitulada “7.1 Testes de serviço com mocks”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.
7.2 Cenários de teste críticos
Seção intitulada “7.2 Cenários de teste críticos”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. |
7.3 Teste do algoritmo puro
Seção intitulada “7.3 Teste do algoritmo puro”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.
7.4 Dados de desenvolvimento local
Seção intitulada “7.4 Dados de desenvolvimento local”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çãoINSERT 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.
7.5 Submissão manual pelo endpoint
Seção intitulada “7.5 Submissão manual pelo endpoint”# 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 geradapsql -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.
8. Alinhamento com o MVP
Seção intitulada “8. Alinhamento com o MVP”8.1 MVP obrigatório
Seção intitulada “8.1 MVP obrigatório”| 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 |
8.2 Simplificações válidas no MVP
Seção intitulada “8.2 Simplificações válidas no MVP”| 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. |
8.3 Adiar para Fase 2
Seção intitulada “8.3 Adiar para Fase 2”| 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. |
Referências cruzadas
Seção intitulada “Referências cruzadas”| 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 |