Appearance
@fc/rmi-api
API REST de cálculo de RMI (Renda Mensal Inicial) para benefícios do RGPS. Expõe OpenAPI em /_openapi.json e deixa /_swagger disponível apenas em desenvolvimento local.
Esta aplicação é um deployment separado por razões operacionais (cálculo de RMI é intensivo em CPU/memória). Do ponto de vista do contrato público, todos os endpoints vivem sob o namespace /rgps/api/v1/* e compartilham o módulo rgps com a prev-api.
Todas as respostas JSON seguem o envelope PDPJ-Br:
json
{
"status": "ok",
"code": "200",
"messages": [],
"result": {
/* ... */
}
}Geração do spec
Para regenerar o spec OpenAPI localmente:
bash
pnpm --filter @fc/rmi-api spec:genO comando usa as anotações @swagger dos arquivos em server/routes/** e os tipos declarados em nuxt.config.ts.
O contrato público canônico é /_openapi.json; principal.json é a fonte gerada no build. Ao servir o documento, o runtime acrescenta a origem atual, a proteção por API key e os schemas do envelope de erro, exclui rotas internas e impede cache compartilhado com Cache-Control: no-store. O antigo public/openapi-spec.json não é consumido pela aplicação e está excluído do contexto Docker.
API key estrutural
A proteção por x-api-key é declarativa por path, sem anotação nos handlers, e fica ligada por padrão. O nuxt.config.ts fixa o glob /rgps/api/v1/calculos-rmi/**; assim, a rota-base e todas as suas subrotas exigem a chave sem depender de variáveis de ambiente de deploy.
Os GETs de dados de referência /rgps/api/v1/fundamentos e /rgps/api/v1/marcos-temporais não pertencem a esse glob e permanecem públicos.
A chave é validada pela plataforma contra o cadastro central, com cache curto. Em desenvolvimento local, o assistente pode desligar essa proteção para facilitar testes; fora de dev e test, esse override interrompe o boot e o caminho protegido continua fazendo parte da configuração estrutural da app.
Fail-fast no boot: se o glob não casar com nenhuma rota registrada, a subida da app falha com mensagem clara.
Ver @fc/base-api/README.md para a mecânica completa.
Telemetria de uso
A rmi-api registra telemetria best effort no fim da resposta HTTP. O envio não participa do fluxo principal; falhas no backoffice entram apenas no log operacional. Quando a rota estiver protegida por API key, o evento reaproveita event.context.apiKeyValidada e preserva o idTokenApiKey sem revalidar a chave.
Eventos emitidos:
| Ação | Endpoints |
|---|---|
rgps.rmi.api | cálculo RMI JSON principal |
rgps.rmi.api.stream | cálculo RMI via SSE |
rgps.rmi.api.async | criação de job assíncrono JSON e conclusão/erro do job |
rgps.rmi.api.arquivo-calc | cálculo RMI a partir de .calc |
rgps.rmi.api.arquivo-calc.stream | cálculo RMI via SSE a partir de .calc |
rgps.rmi.api.arquivo-calc.async | criação e conclusão/erro de job assíncrono a partir de .calc |
rgps.rmi.api.desvinculada | cálculo desvinculado JSON ou .calc |
rgps.rmi.api.por-fundamento | cálculo por fundamento |
rgps.rmi.api.job-status | consulta do estado de um job |
rgps.rmi.api.job-resultado | obtenção do resultado de um job |
rgps.rmi.api.job-cancelar | cancelamento de um job |
rgps.rmi.api.fundamentos | listagem de fundamentos disponíveis |
rgps.rmi.api.marcos-temporais | listagem de marcos temporais disponíveis |
rgps.documentos.api.arquivo-calc | leitura isolada de .calc, vinculada ou desvinculada |
O dicionário de @fc/comum é a fonte canônica dos nomes. Um teste de contrato compara o mapa com as rotas físicas nas duas direções, valida cada ação para a origem rmi-api e impede que uma rota nova fique silenciosa. Eventos de conclusão de job carregam apenas fase e endpoint; payload, conteúdo e nome do documento não entram na telemetria. Nos logs operacionais, os prefixos rmi.job.* e arquivo-calc.rmi.job.* distinguem a origem sem depender do nome do arquivo.
Benchmark de latência
Há um benchmark reproduzível para comparar os endpoints JSON e SSE equivalentes:
bash
pnpm --filter @fc/rmi-api bench:latency -- --cenario implementacao-postergados --runs 8 --warmup 2Parâmetros úteis:
--listar-cenarios: lista os cenários disponíveis--base-url http://127.0.0.1:8003: aponta para uma instância já em execução--execucao fundamento --fundamento EC_103_ART_20: executa o cálculopor-fundamentopara o fundamento informado--marco-temporal NA_DIB: fixa o cálculo em um único marco temporal (valor fora do catálogo deGET /marcos-temporaisé rejeitado)--output ./tmp/benchmark-rmi.json: salva o relatório em JSON
Métricas coletadas:
TTFB: tempo até os headers da respostaTTLB: tempo até o fim da resposta JSONTTFE: tempo até o primeiro evento útil do SSEPrimeiro parcial: tempo até o primeirofundamento_calculadooubloco_finalizadoTTLE: tempo até o evento finalfim
O benchmark usa as amostras de test/amostras/*.json e as converte para CalculoRmiApi com lerArquivoTc, mantendo o payload o mais próximo possível dos casos reais do projeto.
Dependência externa
A rmi-api consome a API pública de índices (endpoints /indices/api/v1/*) para obter a tabela de indexadores utilizada nos cálculos. O endereço efetivo é fornecido pela implantação.
Se a API de índices estiver indisponível, os endpoints de cálculo retornam 503 com o seguinte corpo:
json
{
"error": true,
"statusCode": 503,
"statusMessage": "API de índices indisponível",
"message": "API de índices indisponível"
}A retenção e a capacidade dos jobs assíncronos são limites operacionais configurados pela implantação. Ao atingir o teto de registros, a API descarta primeiro o resultado terminal mais antigo; jobs ativos não são expulsos. Os cálculos são executados fora do fluxo HTTP principal, mantendo o health check responsivo durante operações longas.
Endpoints
Todos os caminhos são servidos sob o namespace /rgps/api/v1/*.
Cálculo principal (JSON)
| Método | Caminho | Descrição |
|---|---|---|
POST | /rgps/api/v1/calculos-rmi | Calcula a RMI para todos os benefícios aplicáveis |
POST | /rgps/api/v1/calculos-rmi/async | Cria job assíncrono de cálculo de RMI (retorna 202 + jobId) |
POST | /rgps/api/v1/calculos-rmi/stream | Calcula a RMI com streaming SSE por bloco e fundamento |
POST | /rgps/api/v1/calculos-rmi/por-fundamento | Calcula a RMI apenas para o fundamento informado |
POST | /rgps/api/v1/calculos-rmi/desvinculada | Calcula RMI desvinculada com parâmetros fornecidos pelo usuário |
Fonte externa: arquivo .calc
| Método | Caminho | Descrição |
|---|---|---|
POST | /rgps/api/v1/calculos-rmi/arquivo-calc/ler | Lê arquivo .calc de RMI canônica (períodos) e devolve o JSON pronto para o cálculo canônico; arquivo de RMI desvinculada responde 422 orientando ler-desvinculada |
POST | /rgps/api/v1/calculos-rmi/arquivo-calc/ler-desvinculada | Lê arquivo .calc de RMI desvinculada (coeficiente e tempo informados) e devolve o JSON pronto para o cálculo desvinculado; arquivo de RMI canônica responde 422 orientando ler |
POST | /rgps/api/v1/calculos-rmi/arquivo-calc/calcular | Leitura + cálculo em uma única chamada |
POST | /rgps/api/v1/calculos-rmi/arquivo-calc/calcular/async | Leitura + cálculo em job assíncrono (retorna 202 + jobId) |
POST | /rgps/api/v1/calculos-rmi/arquivo-calc/calcular/stream | Leitura + cálculo com streaming SSE |
POST | /rgps/api/v1/calculos-rmi/arquivo-calc/calcular-desvinculada | Leitura + cálculo desvinculado em uma única chamada |
Como escolher o leitor. O arquivo .calc de Benefícios RGPS guarda um de dois tipos de RMI: a canônica, derivada dos períodos e das regras de tempo, e a desvinculada (salário-pura), em que o usuário informou coeficiente e tempo. arquivo-calc/ler → POST /rgps/api/v1/calculos-rmi; arquivo-calc/ler-desvinculada → POST /rgps/api/v1/calculos-rmi/desvinculada. Cada leitor recusa (422) o arquivo do outro tipo, em vez de devolver um payload que recalcularia a RMI como zero; arquivo-calc/calcular despacha sozinho e não exige essa escolha.
Jobs (operações assíncronas)
| Método | Caminho | Descrição |
|---|---|---|
GET | /rgps/api/v1/calculos-rmi/jobs/{jobId} | Consulta status, progresso e resultado eventual de job assíncrono |
GET | /rgps/api/v1/calculos-rmi/jobs/{jobId}/resultado | Obtém apenas o resultado final de job assíncrono concluído |
DELETE | /rgps/api/v1/calculos-rmi/jobs/{jobId} | Cancela job pendente ou em processamento da mesma chave |
Metadados
| Método | Caminho | Descrição |
|---|---|---|
GET | /rgps/api/v1/fundamentos | Lista os fundamentos aceitos nos endpoints filtrados |
GET | /rgps/api/v1/marcos-temporais | Lista os marcos temporais aceitos em marcoTemporal |
Os endpoints de upload de arquivo aceitam multipart/form-data com o arquivo no campo file. Os demais aceitam application/json, com exceção dos endpoints SSE, que recebem JSON (ou multipart/form-data para os que têm .calc) e respondem com text/event-stream.
A query opcional numeroMaximoIteracoes limita a quantidade de iterações de descarte nos cálculos canônicos JSON e .calc, inclusive nos modos stream e async, e no cálculo por fundamento. Quando omitida, a API usa 120 como padrão para evitar payloads excessivamente grandes em casos com muitos salários descartáveis.
As rotas síncronas que efetivamente calculam aceitam também permalink=true (ou 1). Quando solicitada, a API persiste um snapshot imutável e reabrível no prev-tc e acrescenta permalink — uma URL pública completa — no topo do envelope, sem alterar result. Omitir a query, ou usar false/0, preserva integralmente o contrato anterior. Os modos async e stream não oferecem esse campo. O snapshot segue a retenção longa da plataforma, atualmente dez anos; o opt-in não amplia os limites de entrada de 10 MiB para JSON e 5 MiB para .calc. Valor diferente dos quatro aceitos responde 400, indisponibilidade da capacidade responde 503 e uma resposta interna sem URL válida responde 502.
As mesmas cinco rotas síncronas aceitam também a query opcional demonstrativo=true (ou 1): o envelope ganha demonstrativoPdf — o demonstrativo do cálculo em PDF, na identidade visual da calculadora, com conteúdo em base64, nome de arquivo sugerido, quantidade de páginas e tamanho em bytes (schema DemonstrativoPdfApi no OpenAPI). As rotas de RMI canônica, por fundamento e .calc devolvem ainda demonstrativoDescartesPdf, o demonstrativo dos cálculos a cada descarte, quando alguma hipótese descartou competências; a RMI desvinculada rende um PDF só. O PDF sempre embute um permalink do cálculo no card de reprodução — por isso a geração cria um snapshot como o do permalink=true, e as duas queries podem ser combinadas sem criação duplicada; a query permalink decide apenas se a URL também sai como campo separado. Cálculo interrompido pelo limite de iterações não vira PDF: com a query, a rota responde 422. Os mesmos códigos valem para a capacidade de permalink (503/502), e falha na geração do PDF responde 500.
Para cálculos longos com upload .calc, prefira:
POST /rgps/api/v1/calculos-rmi/arquivo-calc/calcular/streamquando o cliente puder manter a conexão aberta e quiser progresso em tempo real.POST /rgps/api/v1/calculos-rmi/arquivo-calc/calcular/asyncquando o cliente precisar apenas receber umjobIde consultar o resultado depois via polling.
Limitação conhecida da trilha assíncrona: os jobs ficam no storage local do processo da rmi-api. Em ambientes com múltiplas instâncias, é necessário afinidade de sessão, ou futuramente migrar essa trilha para storage/fila distribuídos.
Cada job pertence à identidade estável (idToken) da API key que o criou. Consulta, resultado e cancelamento exigem a mesma identidade; outra chave recebe o mesmo 404 de um identificador inexistente, sem confirmar a existência do job. A resposta pública expõe somente estado, datas, progresso, erro e resultado eventual — o payload interno de criação não faz parte do contrato público.
As rotas async aceitam o header opcional Idempotency-Key (até 128 caracteres). Repetir a mesma requisição com a mesma chave devolve o jobId original; reutilizá-la com conteúdo diferente responde 409. Quando a capacidade estiver inteiramente ocupada por jobs ativos, a fila de cálculos ou a cota de jobs ativos da chave estiver cheia, a API responde 429; nos limites de jobs, Retry-After: 5 orienta uma nova tentativa curta.
Fechar uma conexão SSE cancela o cálculo correspondente. DELETE /jobs/{jobId} faz o mesmo para jobs pendentes ou em processamento; repetir o cancelamento é seguro, enquanto jobs já concluídos ou encerrados com erro respondem 409.
Semântica HTTP dos jobs
| Rota | Situação | HTTP | status |
|---|---|---|---|
POST /calculos-rmi/async | job criado | 202 | in-progress |
POST /calculos-rmi/arquivo-calc/.../async | job criado | 202 | in-progress |
GET /jobs/{jobId} | pendente / processando | 200 | in-progress |
GET /jobs/{jobId} | concluído | 200 | ok |
GET /jobs/{jobId} | cancelado | 200 | ok |
GET /jobs/{jobId} | inexistente ou de outra chave | 404 | error |
GET /jobs/{jobId}/resultado | concluído | 200 | ok |
GET /jobs/{jobId}/resultado | pendente / processando / falhou / cancelado | 409 | error |
GET /jobs/{jobId}/resultado | inexistente ou de outra chave | 404 | error |
DELETE /jobs/{jobId} | pendente / processando / já cancelado | 200 | ok |
DELETE /jobs/{jobId} | concluído / falhou | 409 | error |
Resultado de job assíncrono só é entregue em /resultado quando o job está concluído; nos demais estados a resposta é 409 Conflict com mensagem descritiva (alinhada ao Swagger). Para polling genérico — que precisa distinguir "ainda processando" de "já terminou" — use GET /jobs/{jobId} e observe status.
Sinalização de conclusão e truncamento
Cada bloco calculado inclui conclusao.completo. Se o limite interromper o descarte antes do fim, o bloco informa:
json
{
"conclusao": {
"completo": false,
"motivoInterrupcao": "LIMITE_ITERACOES",
"limitesAtingidos": { "numeroMaximoIteracoes": 120 }
}
}O evento final fim do SSE e o status do job assíncrono expõem a mesma conclusao, agregada para todos os blocos. Um resultado completo contém apenas { "completo": true }.
Por compatibilidade, respostas HTTP síncronas truncadas também incluem:
x-fc-rmi-iteracoes-limitadas: true
x-fc-rmi-max-iteracoes: <valor aplicado>O log de auditoria registra iteracoesLimitadas: true no evento rmi.calculado. Clientes devem preferir o campo do corpo, que também sobrevive a fachadas como o MCP, para decidir se reexecutam com um limite maior ou apresentam ressalva ao usuário.
Validação de entrada e contrato de erro
Rotas JSON e multipart validam o payload com schemas Zod (apps/rmi-api/server/utils/esquemas.ts) e uploads com validarUploadUnico. Em falha, a resposta é:
- JSON malformado, Zod inválido,
ErroDominio, exceção conhecida do core → 400 + envelope PDPJ commessagesdescritivas (caminho: mensagempara Zod); - upload acima do teto de 5 MiB → 413;
- arquivo
.calclegível, mas incompatível com o endpoint → 422; - exceção não classificada → 500 + envelope genérico (erro registrado via
logOperacional.error); - sempre via
tratarErroRota(event, erro)de@base-api/server/utils/erros— nuncacreateErrorcru.
Os uploads multipart aceitam tanto content-length quanto transfer-encoding: chunked, sem alterar o teto nem a resposta 413.
Espécie do benefício. opcoesContagem.especie é obrigatória em todas as entradas JSON (canônica, por fundamento, desvinculada e PrevJud): sem ela a resposta é 400 com a mensagem que diz onde informá-la e os códigos aceitos — a API não presume espécie alguma. Aceita todas as espécies do dicionário (aposentadorias 41, 42, 46 e 57; incapacidade 31, 32, 91 e 92; auxílio-acidente 36, 94 e 95; pensões 21 e 93; auxílio-reclusão 25; salário-maternidade 80; amparos 87 e 88), e cada uma segue as próprias regras de RMI. Na RMI desvinculada isso vale igual: com especie: 94, o auxílio-acidente sai com coeficiente de 50%, piso proporcional e sem fator previdenciário; os benefícios não programados aceitam fatoGerador (DII, óbito) quando ele difere da DER. Num arquivo .calc, o campo é parametrosGerais.especie, e o arquivo sem ele também responde 400.
O campo opcional criterioSalarioMaternidade aceita UM_DOZE_AVOS ou MEDIA_SIMPLES_DOZE_ULTIMOS e mantém a escolha presente em um Arquivo .calc. A origem dos períodos usa valores nominais (CALCULO_INSS, CNIS, USUARIO, NENHUMA, PREVJUD ou MULTIPLA); valores divergentes retornam 400. Só CNIS, PREVJUD e CALCULO_INSS têm coluna própria no salário; USUARIO, MULTIPLA e a origem omitida entram como valor digitado pelo usuário, e o resultado os rotula como USUARIO — o número não muda. Nos resultados, cada benefício traz fundamento (a citação legal legível, EC 103, art. 20) e chaveFundamento (a chave, EC_103_ART_20) — a mesma aceita em POST /calculos-rmi/por-fundamento e listada em valor por GET /fundamentos, que devolve a citação em rotulo. As chaves de diferencas nos descartes usam a citação.
Convenções
Princípio central: um endpoint = um contrato (input único, output único).
- Recurso (plural hifenizado):
calculos-rmi. - Variantes de cálculo (sub-path):
por-fundamento,desvinculada. - Fonte externa (sub-recurso aninhado):
arquivo-calc/ler,arquivo-calc/calcular. - Modo de entrega (sub-path):
async(202 +jobId),stream(SSE). Apenascalculos-rmicanônico earquivo-calc/calcularcanônico têm modos async/stream; as variantes são exclusivamente síncronas.
Implementação interna
Os fluxos de cálculo desta app usam a superfície central do core para manter a mesma normalização entre web, endpoints JSON e leitura de .calc:
- entradas JSON usam
DadosCalculo.deApiRmi(...)eDadosCalculo.deApiRmiDesvinculada(...) - entradas
.calcusamDadosCalculo.deArquivoTc(...) - saídas serializadas usam
CalculoRmi.paraApiBloco(...)eCalculoRmi.paraApiDesvinculada(...)
Os módulos legados em @fc/calculos-previdenciarios/utilidades-apis/* seguem exportados como compatibilidade transitória, mas novas integrações internas devem preferir essa superfície centralizada.
Logs
A aplicação emite logs estruturados em JSON via a classe Logger de @fc/utils. Todos os logs são escritos em stdout, prontos para ingestão por qualquer coletor (Loki, Elasticsearch, etc.).
Tipos de log
| Tag | Descrição |
|---|---|
rmi-api | Log operacional — registra toda requisição recebida |
rmi-api, auditoria | Log de auditoria — registra cada operação de cálculo/leitura concluída |
Os loggers vêm de @base/server/utils/loggers e usam como primeira tag o valor de nomeApp declarado em app/app.config.ts (aqui: rmi-api):
logOperacional— usado pelo middleware de log da layer@fc/base-apipara registrar todas as requisições com a URL acessada.logAuditoria— usado em cada endpoint para registrar a operação realizada.
Filtragem
Para isolar logs de auditoria em um coletor centralizado, filtre pela presença da tag auditoria:
json
{ "tags": ["rmi-api", "auditoria"], ... }