Você lança e altera produto de crédito por API, e cada contrato continua valendo sob a versão que estava publicada no dia em que foi assinado — mesmo depois de a taxa, o IOF ou a regra mudarem.
- Financeiras e fintechs de crédito que operam várias tabelas por canal, convênio ou correspondente
- Bancos digitais e SCDs que precisam alterar condição de produto sem esperar janela do core banking
- Times de risco e produto que respondem auditoria sobre a condição vigente na data de cada contrato
- Planilha de tabelas de produto compartilhada entre produto, risco e TI
- Tabela de parâmetros de produto codificada dentro da esteira de originação
- Módulo de cadastro de produto customizado dentro do core banking legado
- Um core banking — não abre conta, não escritura contrato, não movimenta saldo
- Uma calculadora financeira — quem calcula parcela, IOF e CET é o Calculations Engine
- Um motor de precificação por risco — quem define spread por faixa é o Pricing Engine
- Um catálogo de produtos de varejo com estoque e preço — isso é o building block Products
01Resumo executivo
O Banking Product Portfolio é onde a sua instituição descreve o que ela vende: quais produtos de crédito, depósito, conta, investimento e seguro existem, com que faixa de valor, que prazo, que taxa e para quem. Ele guarda esse catálogo, versiona cada mudança e responde, para qualquer data do passado, sob que condição um produto estava sendo oferecido.
Na prática, resolve um problema que dói toda vez que uma regra muda. Quando o governo alterou as alíquotas de IOF de crédito para pessoa jurídica em maio de 2025 (Decreto 12.466/2025), toda financeira precisou de duas coisas ao mesmo tempo: a condição nova valendo para contrato novo, e a condição antiga preservada para contrato já assinado. Aqui isso é uma versão publicada e uma versão depreciada — não uma coluna sobrescrita com um UPDATE que ninguém consegue desfazer.
Está em beta desde fevereiro de 2026. As cinco tabelas, as trinta e quatro rotas e o ciclo de vida de versão estão implementados e cobertos por testes unitários e de integração. Ainda não tem host publicado em staging nem cálculo financeiro embutido — a simulação resolve parâmetros e elegibilidade, e quem calcula parcela, IOF e CET é o Calculations Engine. Leia a §15 antes de prometer qualquer coisa a cliente.
| Atributo | Valor |
|---|---|
| Identificador | banking-product-portfolio |
| Categoria | Financeiro |
| Escopo | Tenant (exige organizationId no token em todas as 34 rotas) |
| Porta (standalone) | 3018 |
| Path alias | @banking-product-portfolio |
| Prefixo HTTP | /banking-product-portfolio |
| Schema PostgreSQL | banking_portfolio |
| Status | Beta desde 2026-02 |
| Depende de | PostgreSQL, IAM |
02O problemanegócio
O cenário. Uma financeira vende crédito por vários canais. O consignado INSS tem uma tabela, o consignado CLT tem outra, o correspondente do interior negocia teto de prazo menor, e a campanha de fim de ano tem taxa promocional por sessenta dias. Isso não é um produto — são dezenas de combinações da mesma coisa, e todas mudam com frequência.
O que trava hoje.
- A condição do produto vive espalhada. Parte está em planilha do time de produto, parte em constante no código da esteira, parte em tabela do core banking. Quando as três discordam, quem ganha é a que estiver no caminho da requisição naquele dia.
- Alterar um parâmetro exige deploy. Mudar o teto de prazo de 84 para 96 meses vira ticket, sprint e janela de release. O time comercial pede na segunda e recebe no mês seguinte.
- O contrato antigo perde a referência. Sobrescrever a taxa do produto altera, retroativamente, a resposta para a pergunta "sob que condição este contrato foi assinado". Quando o cliente reclama no Procon dois anos depois, não há como reconstruir.
- Mudança regulatória vira mutirão. O IOF mudou duas vezes em 2025, por Decreto 12.466 e Decreto 12.467, e a nova lei do consignado CLT (Lei 15.179/2025, conversão da MP 1.292/2025) reescreveu a forma de contratação. Cada mudança dessas obriga a revisar todo o catálogo, e sem versionamento a revisão é destrutiva.
- Ninguém aprova nada formalmente. A taxa muda porque alguém com acesso ao banco mudou. Não há registro de quem propôs, quem aprovou e quando entrou em vigor.
O custo de não resolver. O custo direto é o tempo até o produto existir. Fornecedores de core moderno vendem exatamente essa diferença: a Mambu afirma em material próprio que o N26 lançou um produto de crédito em seis semanas, contra os 6 a 12 meses típicos de sistema legado (Mambu, estudo de caso N26) — número de marketing do fornecedor, e deve ser lido como tal. O custo indireto é maior e menos visível: sem versão, cada alteração de produto é uma perda silenciosa de rastreabilidade sobre uma carteira de crédito que, no Sistema Financeiro Nacional, somava R$ 7,1 trilhões em janeiro de 2026 (BACEN, Estatísticas Monetárias e de Crédito).
03Proposta de valornegócio
| Antes | Depois |
|---|---|
| Alterar teto de prazo é ticket, sprint e deploy | PATCH no produto, nova versão, publicar — sem release |
| Sobrescrever a taxa apaga a condição anterior | A versão antiga vira DEPRECATED e continua consultável para sempre |
| Cada canal duplica o produto inteiro para mudar dois campos | Variante declara só o que muda e herda o resto |
| "Quem autorizou essa taxa?" é uma conversa | approvedBy e approvedAt gravados, com aprovador ≠ criador exigido pelo código |
Elegibilidade é if espalhado na esteira | Regra campo-operador-valor no catálogo, avaliada por API |
A versão é um snapshot, não um ponteiro. Quando você cria uma versão, o serviço resolve os parâmetros das três camadas — família, produto, variante — e grava o resultado inteiro em JSONB. Alterar o produto depois não muda a versão. É essa escolha que faz o contrato assinado sob a versão antiga continuar tendo uma condição verificável.
A herança evita duplicação sem permitir folga. A família define o padrão da categoria, o produto especializa, a variante ajusta. E a variante só consegue estreitar faixa: tentar oferecer prazo maior que o do produto-pai devolve 400, com a mensagem dizendo exatamente qual campo violou.
A aprovação é código, não processo. POST /versions/:id/approve recusa quando o aprovador é o mesmo usuário que criou a versão. Segregação de funções que sobrevive a troca de time.
O parâmetro é livre, a validação é forte. Os parâmetros ficam em JSONB, então uma categoria nova não exige migração. Mas cada categoria tem schema Zod próprio, validado na gravação: um produto de crédito sem minAmount não entra no banco.
A publicação despublica a anterior sozinha. Publicar uma versão marca a versão publicada anterior do mesmo par produto/variante como DEPRECATED, na mesma operação. Não existe estado com duas versões vigentes.
04Casos de uso reaisnegócio
Caso 1 — Uma mudança de IOF entra em produção sem quebrar contrato antigo Cenário ilustrativo
Contexto. Financeira de crédito pessoal com 14 produtos ativos, operando pessoa física e pessoa jurídica.
A dor. Em maio de 2025 as alíquotas de IOF sobre crédito de pessoa jurídica mudaram, e mudaram de novo dias depois (Decretos 12.466 e 12.467/2025). Numa operação sem versionamento, o time faz UPDATE na tabela de produtos, o número novo passa a valer para tudo e a condição usada nos contratos da semana anterior desaparece. Quando a área de compliance pede o parâmetro vigente na data de cada contrato, a resposta é uma reconstrução manual a partir de backup.
A solução com o BB. O time cria POST /versions para cada produto de PJ afetado, com version: "2.1.0". A criação congela o snapshot com o iofDailyRate novo. Submete para revisão, um segundo usuário aprova, e POST /versions/:id/publish coloca em vigor — o mesmo request marca a 2.0.0 como DEPRECATED. Os contratos anteriores guardam o versionId da 2.0.0, e GET /versions/:versionId devolve o snapshot exato, para sempre.
O resultado. A pergunta "qual era o IOF deste contrato" vira uma chamada de API. A migração de parâmetro deixa de ser destrutiva e a auditoria deixa de depender de restore de backup.
Caso 2 — Cento e vinte correspondentes, um produto Cenário ilustrativo
Contexto. Financeira de consignado que origina através de correspondentes bancários, cada um com teto de prazo e faixa de valor negociados.
A dor. A primeira versão do sistema duplicava o produto inteiro por correspondente. Cento e vinte cópias de doze parâmetros cada. Mudar a alíquota de seguro prestamista significava cento e vinte edições, e sempre sobrava uma esquecida — normalmente descoberta quando um correspondente vendia sob condição que a instituição não podia honrar.
A solução com o BB. Um produto consignado-inss guarda a condição-teto. Cada correspondente vira uma variante com parameterOverrides contendo só o que difere — quase sempre maxTerm e maxAmount. O ParameterResolverService faz o merge profundo família → produto → variante na leitura, e a validação de restrição impede que qualquer variante ofereça prazo maior que o do produto-pai. Alterar o seguro prestamista passa a ser um PATCH no produto.
O resultado. Uma alteração em vez de cento e vinte. E a impossibilidade estrutural de um correspondente vender além do teto — não porque alguém conferiu, mas porque o POST de variante com maxTerm maior devolve 400.
Caso 3 — A esteira pergunta "este cliente pode?" antes de originar Cenário ilustrativo
Contexto. Fintech de crédito que roda pré-análise no aplicativo, antes de puxar bureau, para não gastar consulta paga com quem já não se enquadra.
A dor. As regras de corte — idade mínima, renda mínima, UF permitida, score mínimo — estavam em if dentro do serviço de originação. Mudar o corte de renda exigia deploy, e as regras do produto A e do produto B divergiram sem que ninguém notasse.
A solução com o BB. Cada corte vira uma EligibilityRule com field, operator e value — por exemplo field: "renda", operator: "GTE", value: 2500. A esteira chama POST /simulate com o contexto do cliente e recebe, na mesma resposta, os parâmetros resolvidos do produto e o resultado regra a regra, com valor esperado e valor recebido de cada uma. Só quem passa segue para a consulta paga de bureau.
O resultado. Mudança de corte deixa de ser deploy. E quando o cliente pergunta por que foi recusado na pré-análise, a resposta vem com o nome da regra que reprovou, não com um booleano.
Caso 4 — O mercado já precificou esse problema Referência de mercado
Contexto. O desacoplamento do catálogo de produtos em relação ao core é o argumento central de toda uma categoria de fornecedores. A Mambu popularizou o termo composable banking justamente para descrever produtos montados a partir de blocos configuráveis em vez de código customizado no monolito. A Thought Machine leva ao extremo oposto: o produto é um smart contract em Python, com framework de teste e controle de versão (Thought Machine, sobre desenvolvimento de produto).
A dor do mercado. Os dois estão respondendo à mesma coisa: no core tradicional, produto é configuração enterrada em sistema que ninguém quer tocar, e por isso lançar um leva meses. A Mambu cita seis semanas contra 6 a 12 meses no caso do N26 (estudo de caso) — material do próprio fornecedor, sem auditoria independente, e deve ser tratado como ordem de grandeza e não como benchmark.
Como a Catalisa endereça. O caminho é o mesmo, o preço de entrada não. Mambu, Vault Core e Temenos entregam o catálogo dentro de um core banking completo: para ganhar o product factory você troca a conta, o ledger e a liquidação. Aqui o catálogo é um building block isolado, com banco próprio no schema banking_portfolio, que conversa por HTTP e roda ao lado do core que a instituição já opera.
O resultado. A instituição ganha versionamento e herança de produto sem abrir um projeto de substituição de core. O trade-off honesto está na §5: nós não temos ledger, e eles têm.
05Mercado e diferenciaisnegócio
Panorama. O catálogo de produtos sempre existiu — ele estava dentro do core banking, como configuração, e por isso alterá-lo custava uma janela de release. A partir de 2015 uma geração de fornecedores separou essa peça e deu nome a ela: product factory, product engine, composable banking. Mambu, Thought Machine e 10x nasceram desse recorte; Temenos, FIS e os core brasileiros reagiram modernizando o módulo de produto de dentro para fora. O consenso técnico é claro e nada polêmico: produto financeiro deve ser configuração versionada, não código.
O que ninguém desses fornecedores oferece é a peça sozinha. Todos vendem o core inteiro. Uma financeira brasileira que já opera um core adequado, e que só precisa parar de gerenciar tabela de produto em planilha, não tem uma opção proporcional ao problema. É esse recorte que este building block ocupa.
| Critério | Catalisa Portfolio | Mambu | Thought Machine Vault Core | Temenos Transact | Matera |
|---|---|---|---|---|---|
| Escopo | Só o catálogo de produto | Core banking completo | Core banking completo | Core banking completo | Core banking completo |
| Ledger e escrituração de contrato | Não tem | Sim | Sim | Sim | Sim |
| Definição de produto | JSONB + Zod, por API | Configuração no product engine | Smart contract em Python | Configuração do Transact | Configuração + APIs |
| Versão imutável de produto | Sim, snapshot por versão | Sim | Sim, com versionamento de código | Depende da implantação | Não confirmado publicamente |
| Aprovação com segregação de funções | Sim, no código | Via workflow configurável | Via pipeline de código | Via workflow | Não confirmado publicamente |
| Herança família → produto → variante | Sim, deep merge com restrição | Product templates | Composição de contratos | Hierarquia de produto | Não confirmado publicamente |
| Precificação por risco | Não (é o Pricing Engine) | Parcial | Via smart contract | Sim | Parcial |
| Cálculo de parcela, IOF e CET | Não (é o Calculations Engine) | Sim | Sim, no smart contract | Sim | Sim |
| Preço | Precificação em definição | Não publicado | Não publicado | Não publicado | Não publicado |
| Porte de entrada | Building block avulso | Projeto de core | Projeto de banco | Projeto de banco | Projeto de core |
Nenhum dos quatro fornecedores publica tabela de preço. As linhas marcadas como "não confirmado publicamente" são exatamente isso: não encontramos documentação pública que confirme o comportamento, e não vamos afirmar o contrário. Consulta feita em 2026-08.
Nossos diferenciais
- O snapshot é criado no rascunho, não na publicação. No instante em que a versão nasce, os parâmetros resolvidos das três camadas são congelados em JSONB e nunca mais são reescritos — nem por
PATCHno produto, nem por publicação, nem por depreciação. É difícil de copiar porque não é uma funcionalidade, é uma decisão de modelagem: quem guarda referência em vez de cópia não conserta isso sem migrar dados. - A segregação de funções é uma linha de código, não uma promessa de processo.
approveVersioncomparaversion.createdBycom ouserIddo token e devolve erro quando são iguais. Não há configuração que desligue isso. - A variante restringe, nunca amplia.
validateRangeRestrictionscompara cadamin/maxdo override com o do produto-pai e recusa a gravação que abrir a faixa. Em produto de crédito, essa é a diferença entre um canal negociar dentro do mandato e um canal vender o que a instituição não pode honrar. - É a peça, não a plataforma. Roda ao lado do core existente, com banco próprio, sem exigir que a instituição troque o que já funciona.
Quando escolher o concorrente. Se a sua instituição vai trocar o core, escolha o core — Mambu, Vault Core, Temenos ou Matera entregam catálogo, ledger, escrituração e liquidação integrados, e nós não temos ledger nenhum. Se o produto precisa de comportamento programável de verdade (juros que se recalculam no ciclo, condições que reagem a evento de conta, taxação embutida no próprio contrato), o modelo de smart contract da Thought Machine é mais expressivo do que campo-operador-valor e continuará sendo. Se a exigência é regulatório brasileiro ponta a ponta — Pix, SPB, contabilidade, envio de documentos ao BACEN — a Matera já tem essa estrada. E se você precisa hoje de motor de decisão com árvore, tabela DMN e chamada a bureau, o lugar disso na Catalisa é o Decision Platform, não aqui: a elegibilidade deste building block é deliberadamente simples. Este bloco ganha quando o problema é catálogo e versão de produto ao lado de um core que fica.
06Modelo de cobrança e ROInegócio
Unidade de cobrança. Precificação em definição. Não há preço fechado para este building block e não vamos inventar um.
O que dispara custo. Os drivers considerados são três, todos com relação direta com o valor entregue:
| Driver | Por que é justo |
|---|---|
| Produtos ativos no catálogo | Mede o tamanho da operação, não o número de usuários que a operam |
| Versões publicadas por mês | Mede a frequência de mudança — quem muda mais é quem mais aproveita o versionamento |
| Chamadas de simulação e elegibilidade | Único driver que escala com volume transacional |
Comparação de custo. Não é possível fazer comparação numérica honesta: nenhum dos quatro análogos publica tabela de preço. Mambu, Thought Machine, Temenos e Matera trabalham com contrato negociado, tipicamente com componente de licença, componente por volume e um projeto de implantação à parte. O que dá para comparar é a forma do custo, e essa diferença é material:
| Catalisa Portfolio | Core banking completo (qualquer um dos quatro) | |
|---|---|---|
| Custo de licença | Precificação em definição | Contrato negociado, não publicado |
| Projeto de implantação | Integração de API | Projeto de meses a anos, com migração de dados |
| Substituição do core atual | Não exigida | Exigida, ou operação em dual-core |
| O que você ganha junto | Só o catálogo | Ledger, conta, liquidação, regulatório |
Estimativa de forma, não de valor, consultada em 2026-08. Nenhum número de fornecedor foi extrapolado aqui porque nenhum é público.
ROI. A conta de guardanapo tem duas linhas. A primeira é o ciclo de alteração de produto: numa operação em que mudar um parâmetro custa um ticket, uma sprint e uma janela de release, cada alteração consome de dias a semanas de calendário de engenharia; aqui vira PATCH mais POST /versions mais publicação, sem deploy. Uma financeira que altera condição de produto de duas a quatro vezes por mês recupera dezenas de dias de calendário por ano.
A segunda linha é a que ninguém orça até precisar: reconstruir a condição vigente de um contrato antigo. Sem versionamento, isso é restore de backup, cruzamento manual e uma resposta que ninguém assina com confiança. Com versionamento, é GET /versions/:versionId. O valor dessa linha não é o tempo economizado — é a diferença entre conseguir e não conseguir responder.
07Arquitetura
HTTP
│
┌───────────────────┴────────────────────────────────────────────────────┐
│ Hono app basePath('/banking-product-portfolio') │
│ applyCommonMiddleware + errorHandler │
│ │
│ /api/v1/banking-product-portfolio/families familiesRouter │
│ /api/v1/banking-product-portfolio/products productsRouter │
│ /api/v1/.../products/:productId/variants variantsRouter │
│ /api/v1/banking-product-portfolio/versions versionsRouter │
│ /api/v1/banking-product-portfolio/eligibility-rules eligibilityRouter│
│ /api/v1/banking-product-portfolio/eligibility (mesmo router) │
│ /api/v1/banking-product-portfolio/simulate simulationRouter │
│ /health │
└───────────────────┬────────────────────────────────────────────────────┘
authMiddleware → requirePermission(P) → requireOrganization
│ Zod parse → ResultAsync<T, AppError>
┌───────────────────┴────────────────────────────────────────────────────┐
│ services/ │
│ FamilyService ProductService VariantService │
│ VersionService máquina de estados + snapshot + segregação │
│ EligibilityService avalia campo-operador-valor em processo │
│ SimulationService orquestra resolução + elegibilidade │
│ ParameterResolverService puro, sem I/O — deep merge das 3 camadas │
└───────────────────┬────────────────────────────────────────────────────┘
│
┌───────────────────┴────────────────────────────────────────────────────┐
│ repositories/ (Prisma) → PostgreSQL, schema "banking_portfolio" │
│ toda query filtra por organizationId E deletedAt: null │
└────────────────────────────────────────────────────────────────────────┘
│
▼ EventPublisher — 22 tipos de evento
banking-product-portfolio.version.published, ...
Decisões não óbvias.
Parâmetro em JSONB, não em coluna tipada. Cinco categorias de produto têm conjuntos de parâmetros completamente diferentes: crédito tem
iofDailyRate, seguro temwaitingPeriodDays. Modelar em colunas exigiria migração a cada categoria nova e deixaria a tabela cheia deNULL. O trade-off aceito é que o banco não valida nada: a garantia de tipo vem do Zod na camada de serviço, e umINSERTfeito direto no Postgres passa por cima dela. Por isso seed e migração de dados precisam ir pela API, não pelo SQL.O snapshot é gravado na criação da versão, não na publicação. É a decisão mais importante do módulo.
createVersionresolve família → produto → variante e grava o resultado emparameterSnapshot.VersionRepository.updatenunca toca esse campo — só mexe emstatus,publishedAt,publishedBy,approvedBy,approvedAt. Consequência prática: entre criar o rascunho e publicá-lo, mudanças no produto não entram na versão. Se você editou o produto depois de criar o rascunho e quer a mudança dentro, crie outra versão.Snapshot completo em vez de diff. Guardar diffs economizaria espaço e exigiria replay para reconstruir estado passado — replay é código, código tem bug, e o bug apareceria justamente na resposta a uma auditoria. Cada versão é autossuficiente. O custo é crescimento linear de armazenamento, mitigado pelo estado
ARCHIVED.ARCHIVEDcomo terminal oculto, separado deDEPRECATED.DEPRECATEDsignifica "não vendo mais, mas ainda sustento" e continua aparecendo na listagem.ARCHIVEDsome da listagem padrão e só volta comfilter[includeArchived]=trueou consulta direta pelo ID. A linha nunca é apagada — a obrigação de retenção é maior que a vontade de limpar.A elegibilidade é in-process, e é simples de propósito. Campo, operador, valor. Sem chamada de rede, sem motor de regras. Regras de corte cobrem a maior parte dos casos de pré-análise, e uma chamada ao Decision Platform para checar se o cliente tem mais de 18 anos seria latência sem benefício. Regra complexa é caso do Decision Platform, e a integração ainda não existe (§15).
"Última versão publicada" é por
publishedAt, não por número.findLatestPublishedordena porpublishedAt: 'desc'. O campoversioné validado como semver (\d+\.\d+\.\d+) mas nunca é comparado como semver. Publicar a1.0.1depois da2.0.0faz da1.0.1a mais recente. Isso é consistente com a semântica de vigência — vale o que foi publicado por último — mas surpreende quem espera ordenação numérica.Todo
DELETEé lógico. As cinco tabelas têmdeletedAt, e todo repositório filtradeletedAt: null. Excluir família ou produto não verifica dependentes: você consegue excluir logicamente um produto que tem versões publicadas. As versões continuam lá, com o snapshot intacto — que é justamente o comportamento desejado para o contrato antigo, mas é uma armadilha para quem espera integridade referencial (§15).
Monolito vs. standalone. Em monolito, src/app.ts monta o app e os serviços vêm do container TypeDI. Em standalone, main.ts sobe o Bun na porta 3018 e o serviço só precisa de PostgreSQL e do JWT_SECRET — a verificação de token é local, sem chamada ao IAM. Não há diferença de comportamento entre os modos: este building block não consome nenhum outro por HTTP.
08Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
Família (BankingProductFamily) | Agrupamento por categoria e segmento. Define defaultParams, o padrão que os produtos herdam. É a família que fixa a categoria — e a categoria decide qual schema Zod valida os parâmetros dos produtos abaixo dela. |
Produto (BankingProduct) | O produto concreto. Pertence a exatamente uma família e carrega parameters em JSONB. |
Variante (BankingProductVariant) | Variação do produto para um canal, convênio ou campanha. Guarda só parameterOverrides — o que difere — e só pode restringir faixas do produto-pai. |
Versão (PortfolioVersion) | Snapshot imutável dos parâmetros resolvidos de um par produto/variante, com número semver e ciclo de vida próprio. É a entidade que um contrato referencia. |
Regra de elegibilidade (EligibilityRule) | Condição campo · operador · valor avaliada contra um contexto. Pode ser do produto (variantId = null) ou de uma variante específica. |
| Parâmetros resolvidos | O resultado do deep merge família → produto → variante. Aparece como resolvedParameters nas respostas de produto e variante, e como parameterSnapshot na versão. |
| Categoria | CREDIT, DEPOSIT, TRANSACTION, INVESTMENT, INSURANCE. Determina o schema de validação dos parâmetros. |
| Segmento | RETAIL, CORPORATE, PRIVATE, SME. Classificação comercial — não afeta validação. |
Modelo de dados — schema banking_portfolio no PostgreSQL. Cinco modelos, todos com organizationId, createdBy e deletedAt.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
BankingProductFamily | banking_product_families | Agrupa por categoria e segmento; define padrões | category, segment, defaultParams (JSONB), único (organizationId, slug) |
BankingProduct | banking_products | Produto concreto com parâmetros | familyId, productType, parameters (JSONB), único (organizationId, familyId, slug) |
BankingProductVariant | banking_product_variants | Variação com overrides parciais | productId, parameterOverrides (JSONB), único (organizationId, productId, slug) |
PortfolioVersion | portfolio_versions | Snapshot imutável versionado | version, status, parameterSnapshot (JSONB), publishedAt/By, approvedAt/By, único (productId, variantId, version) |
EligibilityRule | eligibility_rules | Regra campo-operador-valor | field, operator, value (JSONB), priority, active |
Enumerações — modeladas como String no Prisma e validadas por Zod na aplicação.
| Enumeração | Valores |
|---|---|
ProductCategory | CREDIT · DEPOSIT · TRANSACTION · INVESTMENT · INSURANCE |
CustomerSegment | RETAIL · CORPORATE · PRIVATE · SME |
PortfolioVersionStatus | DRAFT · REVIEW · APPROVED · PUBLISHED · DEPRECATED · SUSPENDED · ARCHIVED |
EligibilityOperator | EQ · NEQ · GT · GTE · LT · LTE · IN · NOT_IN · BETWEEN · REGEX |
Parâmetros por categoria. Cada categoria tem seu schema. Obrigatórios em negrito.
| Categoria | Campos |
|---|---|
CREDIT | minAmount, maxAmount, minInterestRate, maxInterestRate, minTerm, maxTerm, gracePeriodMonths, iofDailyRate, iofAdditionalRate, insuranceRate, registrationTariffRate, amortizationMethods[] |
DEPOSIT | minAmount, maxAmount, minTerm, maxTerm, interestRate, indexer, indexerPercentage, earlyWithdrawalPenaltyRate |
TRANSACTION | monthlyFee, maintenanceFee, freeTransfersPerMonth, transferFee, freeWithdrawalsPerMonth, withdrawalFee, pixLimitPerTransaction, pixDailyLimit |
INVESTMENT | minAmount, maxAmount, managementFeeRate, performanceFeeRate, benchmarkIndex, liquidityDays, riskLevel |
INSURANCE | minPremium, maxPremium, coverageAmount, deductibleAmount, waitingPeriodDays, termMonths, coverageTypes[] |
A hierarquia e a resolução de parâmetros
┌──────────────────────────────────────────────────────────────────────┐
│ FAMÍLIA consignado category: CREDIT │
│ defaultParams: { minTerm: 6, iofDailyRate: 0.000082 } │
└───────────────────────────────┬──────────────────────────────────────┘
│ camada 1 — padrão da categoria
▼
┌──────────────────────────────────────────────────────────────────────┐
│ PRODUTO consignado-inss │
│ parameters: { minAmount: 500, maxAmount: 50000, │
│ minInterestRate: 1.6, maxInterestRate: 1.8, │
│ minTerm: 6, maxTerm: 84 } │
└───────────────────────────────┬──────────────────────────────────────┘
│ camada 2 — sobrescreve a família
┌─────────────┴─────────────┐
▼ ▼
┌───────────────────────────────┐ ┌───────────────────────────────────┐
│ VARIANTE corresp-sul │ │ VARIANTE campanha-natal │
│ overrides: { maxTerm: 60 } │ │ overrides: { maxInterestRate:1.7}│
└───────────────┬───────────────┘ └───────────────┬───────────────────┘
│ camada 3 — só restringe │
▼ ▼
resolvido: maxTerm 60, iof 0.000082 resolvido: maxIR 1.7, maxTerm 84
┌──────────────────────────────────────────────────────────────┐
│ VERSÃO 1.0.0 do par (consignado-inss, corresp-sul) │
│ parameterSnapshot: cópia integral do resolvido acima │
│ ← congelado na CRIAÇÃO. Nada depois altera este JSON. │
└──────────────────────────────────────────────────────────────┘
Regras do merge (ParameterResolverService, puro, sem I/O):
· objetos aninhados são mesclados recursivamente
· arrays e primitivos são substituídos por inteiro (último vence)
· camada nula ou ausente é ignorada
Regra da variante (VariantService.validateRangeRestrictions):
· override de `min*` menor que o do produto → 400
· override de `max*` maior que o do produto → 400
· min efetivo maior que max efetivo → 400
Ciclo de vida da versão
Transições reais, lidas de ALLOWED_TRANSITIONS em version.service.ts. Nenhuma outra é aceita.
POST /versions
│
▼
┌───────────┐
┌──────────▶│ DRAFT │───────────────┐
│ └─────┬─────┘ │
│ │ submit │ publish
│ ▼ │ (atalho: pula revisão)
│ ┌───────────┐ │
│ reject │ REVIEW │ │
└───────────┴─────┬─────┘ │
│ approve │
│ (aprovador ≠ criador)
▼ │
┌───────────┐ │
│ APPROVED │ │
└─────┬─────┘ │
│ publish │
▼ │
┌───────────┐◀──────────────┘
┌──────────▶│ PUBLISHED │
│ └─┬───────┬─┘
│ reactivate │ │ deprecate
│ │ suspend
│ ▼ ▼
┌───────────┐ ┌───────────┐ archive ┌───────────┐
│ SUSPENDED │◀──┘ │────────────▶│ ARCHIVED │
└───────────┘ │DEPRECATED │ │ (terminal)│
└───────────┘ └───────────┘
Efeitos colaterais que o diagrama não mostra:
· publish → marca a versão PUBLISHED anterior do mesmo par
(productId, variantId) como DEPRECATED, no mesmo request.
Nunca há duas versões vigentes.
· approve → grava approvedBy e approvedAt; recusa se createdBy == userId.
· reject → volta para DRAFT e limpa approvedBy e approvedAt.
· reactivate → 409 se já existir outra versão PUBLISHED do mesmo par.
· ARCHIVED não tem saída. É terminal de verdade.
Transições que NÃO existem (devolvem 400):
SUSPENDED → DEPRECATED (reative antes, depois deprecie)
DRAFT → ARCHIVED (só DEPRECATED arquiva)
PUBLISHED → DRAFT (não há despublicar; suspenda)
qualquer → o mesmo estado atual → 409 CONFLICT
09Referência da API
Prefixo completo: /banking-product-portfolio/api/v1/banking-product-portfolio. O segmento aparece duas vezes — o basePath do app e o caminho de montagem dos routers repetem o nome do módulo. É assim no código; as rotas abaixo estão escritas por inteiro para não haver dúvida.
Todas as 34 rotas exigem authMiddleware (Bearer JWT), requirePermission(...) e requireOrganization. Token sem organizationId recebe 403 antes de a regra de negócio rodar.
Em desenvolvimento local (monolito), a base é http://localhost:3000. Este building block ainda não tem host publicado em staging (§15).
Famílias — /families
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/families | Cria família | PORTFOLIO_FAMILY_CREATE |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/families | Lista famílias, paginado | PORTFOLIO_FAMILY_READ |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/families/:familyId | Busca família | PORTFOLIO_FAMILY_READ |
PATCH | /banking-product-portfolio/api/v1/banking-product-portfolio/families/:familyId | Atualiza família | PORTFOLIO_FAMILY_UPDATE |
DELETE | /banking-product-portfolio/api/v1/banking-product-portfolio/families/:familyId | Exclusão lógica. 204 | PORTFOLIO_FAMILY_DELETE |
Filtros da listagem: filter[category], filter[segment], filter[active]. Paginação: pageNumber, pageSize (máx. 100, padrão 20).
Produtos — /products
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/products | Cria produto | PORTFOLIO_PRODUCT_CREATE |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/products | Lista produtos, com resolvedParameters | PORTFOLIO_PRODUCT_READ |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId | Busca produto, com resolvedParameters | PORTFOLIO_PRODUCT_READ |
PATCH | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId | Atualiza produto | PORTFOLIO_PRODUCT_UPDATE |
DELETE | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId | Exclusão lógica. 204 | PORTFOLIO_PRODUCT_DELETE |
Filtros: filter[familyId], filter[category] (via família), filter[active].
Variantes — /products/:productId/variants
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants | Cria variante | PORTFOLIO_VARIANT_CREATE |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants | Lista variantes, com resolvedParameters | PORTFOLIO_VARIANT_READ |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants/:variantId | Busca variante | PORTFOLIO_VARIANT_READ |
PATCH | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants/:variantId | Atualiza variante | PORTFOLIO_VARIANT_UPDATE |
DELETE | /banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants/:variantId | Exclusão lógica. 204 | PORTFOLIO_VARIANT_DELETE |
Filtro: filter[active].
Versões — /versions
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions | Cria versão em DRAFT e congela o snapshot | PORTFOLIO_VERSION_CREATE |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/versions | Lista versões, paginado | PORTFOLIO_VERSION_READ |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/latest | Última versão publicada de um par produto/variante | PORTFOLIO_VERSION_READ |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId | Busca versão pelo ID | PORTFOLIO_VERSION_READ |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/publish | Publica e deprecia a anterior | PORTFOLIO_VERSION_PUBLISH |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/deprecate | Marca como DEPRECATED | PORTFOLIO_VERSION_PUBLISH |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/archive | Marca como ARCHIVED | PORTFOLIO_VERSION_PUBLISH |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/submit | Envia para revisão | PORTFOLIO_APPROVE |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/approve | Aprova. Aprovador ≠ criador | PORTFOLIO_APPROVE |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/reject | Rejeita e volta para DRAFT | PORTFOLIO_APPROVE |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/suspend | Suspende versão publicada | PORTFOLIO_APPROVE |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/reactivate | Reativa versão suspensa | PORTFOLIO_APPROVE |
Filtros da listagem: filter[productId], filter[variantId], filter[status], filter[includeArchived]. Sem filter[status] e sem includeArchived=true, versões ARCHIVED não aparecem.
submit,approve,reject,suspendereactivateusamPORTFOLIO_APPROVE;publish,deprecateearchiveusamPORTFOLIO_VERSION_PUBLISH. Quem opera o fluxo de publicação e quem opera o fluxo de aprovação recebem permissões diferentes de propósito.
Regras de elegibilidade — /eligibility-rules
O mesmo router está montado em dois caminhos: /eligibility-rules e /eligibility. As seis rotas respondem nos dois. O links.self das respostas usa /eligibility-rules — trate esse como o canônico e o outro como alias legado.
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules | Cria regra | PORTFOLIO_ELIGIBILITY_MANAGE |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules | Lista regras de um produto. filter[productId] obrigatório | PORTFOLIO_ELIGIBILITY_MANAGE |
GET | /banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/:ruleId | Busca regra | PORTFOLIO_ELIGIBILITY_MANAGE |
PATCH | /banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/:ruleId | Atualiza regra | PORTFOLIO_ELIGIBILITY_MANAGE |
DELETE | /banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/:ruleId | Exclusão lógica. 204 | PORTFOLIO_ELIGIBILITY_MANAGE |
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/check-eligibility | Avalia um contexto contra as regras | PORTFOLIO_ELIGIBILITY_MANAGE |
Simulação — /simulate
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /banking-product-portfolio/api/v1/banking-product-portfolio/simulate | Resolve parâmetros e avalia elegibilidade | PORTFOLIO_SIMULATE |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /banking-product-portfolio/health | Identificação e versão do build. Sem autenticação. Não conta como endpoint de negócio. |
POST .../versions
Cria a versão em DRAFT e congela o snapshot naquele instante.
Request
{
"version": "1.0.0",
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"variantId": "9c858901-8a57-4791-81fe-4c455b099bc9"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
version | string | Sim | Semver MAJOR.MINOR.PATCH. Formato validado, ordem não comparada |
productId | string (UUID) | Sim | Produto |
variantId | string (UUID) | Não | Omitido, a versão é do produto sem variante |
O corpo também aceita o envelope JSON:API ({"data":{"attributes":{...}}}) — todos os POST e PATCH deste building block aceitam as duas formas.
Resposta 201
{
"data": {
"type": "portfolio-version",
"id": "6b1d3e0a-...",
"attributes": {
"productId": "3fa85f64-...",
"variantId": "9c858901-...",
"version": "1.0.0",
"status": "DRAFT",
"parameterSnapshot": {
"minAmount": 500, "maxAmount": 50000,
"minInterestRate": 1.6, "maxInterestRate": 1.8,
"minTerm": 6, "maxTerm": 60,
"iofDailyRate": 0.000082
},
"publishedAt": null, "publishedBy": null,
"approvedAt": null, "approvedBy": null,
"createdBy": "b1000000-..."
}
}
}
Erros
| Status | Quando |
|---|---|
400 | version fora do formato semver, ou variante que não pertence ao produto informado |
403 | Falta PORTFOLIO_VERSION_CREATE, ou token sem organizationId |
404 | Produto, variante ou família inexistente na sua organização |
409 | Já existe versão com o mesmo (productId, variantId, version) |
POST .../versions/:versionId/publish
Coloca a versão em vigor. Deprecia automaticamente a versão PUBLISHED anterior do mesmo par (productId, variantId), no mesmo request.
Aceita origem DRAFT, APPROVED ou SUSPENDED. Publicar direto de DRAFT é um atalho legítimo do código — se a sua operação exige revisão, controle isso pela permissão, não pelo estado.
Resposta 200 — a versão com status: "PUBLISHED", publishedAt e publishedBy preenchidos.
Erros
| Status | Quando |
|---|---|
400 | Transição não permitida (por exemplo, de DEPRECATED ou ARCHIVED) |
409 | A versão já está PUBLISHED |
404 | Versão inexistente na sua organização |
POST .../versions/:versionId/approve
Aprova uma versão em REVIEW. Grava approvedBy e approvedAt.
Erros
| Status | Quando |
|---|---|
400 | Versão não está em REVIEW |
400 | O aprovador é o mesmo usuário que criou a versão — Approver must be different from version creator |
409 | A versão já está APPROVED |
GET .../versions/latest
| Parâmetro | Tipo | Obrigatório | Descrição |
|---|---|---|---|
productId | UUID em query string | Sim | Omitido, retorna 400 |
variantId | UUID em query string | Não | Omitido, busca a versão do produto sem variante |
Retorna a versão PUBLISHED com o publishedAt mais recente, ou data: null se não houver nenhuma. Não é a de maior número de versão — é a publicada por último.
POST .../simulate
Resolve os parâmetros vigentes e avalia elegibilidade em uma chamada.
Request
{
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"variantId": "9c858901-8a57-4791-81fe-4c455b099bc9",
"versionId": "6b1d3e0a-1111-2222-3333-444444444444",
"context": { "renda": 4200, "idade": 41, "uf": "SP", "score": 720 }
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
productId | UUID | Sim | Produto |
variantId | UUID | Não | Variante |
versionId | UUID | Não | Com ele, os parâmetros vêm do parameterSnapshot da versão. Sem ele, são resolvidos ao vivo das três camadas |
context | objeto | Não (padrão {}) | Os dados avaliados pelas regras. Aceita caminho com ponto: uma regra com field: "cliente.renda" lê context.cliente.renda |
Resposta 200
{
"data": {
"type": "portfolio-simulations",
"id": "3fa85f64-...:9c858901-...:6b1d3e0a-...",
"attributes": {
"productId": "3fa85f64-...",
"variantId": "9c858901-...",
"versionId": "6b1d3e0a-...",
"resolvedParameters": { "minAmount": 500, "maxTerm": 60, "maxInterestRate": 1.8 },
"eligibility": {
"eligible": false,
"results": [
{ "ruleId": "a1...", "ruleName": "Renda mínima", "field": "renda",
"operator": "GTE", "expectedValue": 2500, "actualValue": 4200, "passed": true },
{ "ruleId": "b2...", "ruleName": "Score mínimo", "field": "score",
"operator": "GTE", "expectedValue": 750, "actualValue": 720, "passed": false }
]
}
}
}
}
eligible é true somente se todas as regras avaliadas passarem. A resposta traz sempre o detalhe regra a regra, com o valor esperado e o recebido — é isso que permite dizer ao cliente por que ele não se enquadrou.
A simulação não calcula valores financeiros. Não há parcela, IOF apurado, CET nem tabela de amortização na resposta. Ela devolve os parâmetros e o veredito de elegibilidade; o cálculo é do Calculations Engine (§12).
Erros
| Status | Quando |
|---|---|
400 | Variante não pertence ao produto, ou versão não corresponde ao par produto/variante informado |
404 | Produto, variante, família ou versão inexistente |
POST .../eligibility-rules
Request
{
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Renda mínima",
"field": "renda",
"operator": "GTE",
"value": 2500,
"priority": 10,
"active": true
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
productId | UUID | Sim | Vai no corpo, não na rota |
variantId | UUID | Não | Regra específica de uma variante |
name | string (1–255) | Sim | Aparece no resultado da simulação — escreva pensando em quem vai ler |
field | string (1–255) | Sim | Chave do contexto. Aceita caminho com ponto |
operator | enum | Sim | Ver tabela abaixo |
value | qualquer | Sim | O tipo depende do operador |
priority | inteiro ≥ 0 | Não (0) | Só ordena o resultado. Não interrompe a avaliação nem pondera |
active | booleano | Não (true) | Regra inativa é ignorada e não aparece em results |
| Operador | Formato do value | Comportamento |
|---|---|---|
EQ / NEQ | qualquer | Igualdade estrutural profunda |
GT / GTE / LT / LTE | número ou string | Comparação; tipos diferentes reprovam |
IN / NOT_IN | array | Pertinência ao array |
BETWEEN | array de 2 itens comparáveis | Inclusivo nas duas pontas |
REGEX | string, ou { "pattern": "...", "flags": "i" } | Só avalia contra string |
Campo ausente no contexto reprova a regra. Se o
fieldnão existe nocontext,passedvemfalseeactualValuevemundefined. Não é erro — é reprovação. Monte o contexto completo.
10Início rápido
Do zero à primeira versão publicada, em desenvolvimento local (monolito, http://localhost:3000). Este building block ainda não tem host em staging.
Os comandos abaixo não foram executados na redação deste documento — o ambiente local não estava disponível. Eles foram escritos a partir dos routers e schemas Zod. Ao rodar pela primeira vez, confira as respostas.
Sobe a infraestrutura e o serviço:
docker-compose up -d postgres redis minio
bun run db:generate && bun run db:migrate && bun run db:seed
bun run dev
1. Autenticar
BASE=http://localhost:3000
API=$BASE/banking-product-portfolio/api/v1/banking-product-portfolio
TOKEN=$(curl -s -X POST $BASE/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
2. Criar a família — é ela que fixa a categoria, e a categoria decide a validação.
FAMILY_ID=$(curl -s -X POST $API/families \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Consignado",
"slug": "consignado",
"category": "CREDIT",
"segment": "RETAIL",
"defaultParams": { "iofDailyRate": 0.000082, "gracePeriodMonths": 0 }
}' | jq -r '.data.id')
3. Criar o produto
PRODUCT_ID=$(curl -s -X POST $API/products \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"familyId\": \"$FAMILY_ID\",
\"name\": \"Consignado INSS\",
\"slug\": \"consignado-inss\",
\"productType\": \"CONSIGNADO\",
\"parameters\": {
\"minAmount\": 500, \"maxAmount\": 50000,
\"minInterestRate\": 1.6, \"maxInterestRate\": 1.8,
\"minTerm\": 6, \"maxTerm\": 84
}
}" | jq -r '.data.id')
4. Ver a herança funcionando
curl -s "$API/products/$PRODUCT_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes.resolvedParameters'
{
"iofDailyRate": 0.000082,
"gracePeriodMonths": 0,
"minAmount": 500,
"maxAmount": 50000,
"minInterestRate": 1.6,
"maxInterestRate": 1.8,
"minTerm": 6,
"maxTerm": 84
}
O iofDailyRate veio da família, o resto do produto. Você não repetiu o IOF em nenhum lugar.
5. Criar e publicar a versão
VERSION_ID=$(curl -s -X POST $API/versions \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"version\":\"1.0.0\",\"productId\":\"$PRODUCT_ID\"}" | jq -r '.data.id')
curl -s -X POST "$API/versions/$VERSION_ID/publish" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes | {version, status, publishedAt}'
{ "version": "1.0.0", "status": "PUBLISHED", "publishedAt": "2026-08-16T14:02:11.000Z" }
6. Confirmar que a versão é imutável
# Muda o teto de prazo do produto
curl -s -X PATCH "$API/products/$PRODUCT_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"parameters":{"minAmount":500,"maxAmount":50000,"minInterestRate":1.6,
"maxInterestRate":1.8,"minTerm":6,"maxTerm":96}}' > /dev/null
# A versão publicada continua com o valor antigo
curl -s "$API/versions/$VERSION_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes.parameterSnapshot.maxTerm'
84
O produto agora aceita 96 meses. A versão 1.0.0 continua dizendo 84 — e o contrato assinado sob ela também. É esse o ponto inteiro deste building block.
Credenciais de desenvolvimento local, de AMBIENTES.md. Nunca use credencial de produção em documentação ou script.
11Receitas
Publicar uma alteração de taxa com trilha de aprovação
Fluxo completo quando a operação exige que uma pessoa proponha e outra aprove.
# 1. (usuário A) Altera o produto
curl -s -X PATCH "$API/products/$PRODUCT_ID" \
-H "Authorization: Bearer $TOKEN_A" -H "Content-Type: application/json" \
-d '{"parameters":{"minAmount":500,"maxAmount":50000,"minInterestRate":1.5,
"maxInterestRate":1.7,"minTerm":6,"maxTerm":84}}'
# 2. (usuário A) Cria a versão — o snapshot congela AGORA, com a taxa nova
V=$(curl -s -X POST $API/versions -H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d "{\"version\":\"1.1.0\",\"productId\":\"$PRODUCT_ID\"}" | jq -r '.data.id')
# 3. (usuário A) Envia para revisão
curl -s -X POST "$API/versions/$V/submit" -H "Authorization: Bearer $TOKEN_A"
# 4. (usuário B) Aprova — precisa ser OUTRO usuário
curl -s -X POST "$API/versions/$V/approve" -H "Authorization: Bearer $TOKEN_B"
# 5. (usuário B) Publica — a 1.0.0 vira DEPRECATED automaticamente
curl -s -X POST "$API/versions/$V/publish" -H "Authorization: Bearer $TOKEN_B"
Armadilhas.
- A ordem dos passos 1 e 2 importa. O snapshot congela na criação da versão. Se você criar a versão antes de alterar o produto, a versão sai com os valores antigos e não há como corrigir — crie outra.
- Usuário A não consegue aprovar. O passo 4 com
$TOKEN_Adevolve400comApprover must be different from version creator. Não há como desligar. submit,approveerejectexigemPORTFOLIO_APPROVE;publishexigePORTFOLIO_VERSION_PUBLISH. São permissões diferentes. Um usuário só de aprovação não publica.- Não é preciso passar por revisão:
publishaceitaDRAFTdireto. Se o seu controle exige revisão, restrinjaPORTFOLIO_VERSION_PUBLISHa quem publica depois de aprovar.
Criar uma tabela específica para um canal
# Variante que só reduz o teto de prazo
curl -s -X POST "$API/products/$PRODUCT_ID/variants" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Correspondente Sul",
"slug": "corresp-sul",
"parameterOverrides": { "maxTerm": 60 }
}'
# Versão do par (produto, variante)
curl -s -X POST $API/versions -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"version\":\"1.0.0\",\"productId\":\"$PRODUCT_ID\",\"variantId\":\"$VARIANT_ID\"}"
Armadilhas.
- Ampliar faixa é recusado.
{"maxTerm": 96}num produto commaxTerm: 84devolve400, e a mensagem diz qual campo e quais valores. Isso é intencional. - A versão do produto e a versão da variante são independentes.
1.0.0do produto sem variante e1.0.0do par produto/variante são duas linhas distintas — a chave única é(productId, variantId, version).GET /versions/latestsemvariantIdbusca a do produto sem variante, não a mais recente de qualquer variante. - O override é parcial e valida contra o schema parcial da categoria. Você só declara o que muda; o resto vem do merge.
Reagir a uma mudança regulatória em todo o catálogo
Quando o IOF ou uma alíquota muda, o padrão é: alterar a família (o parâmetro compartilhado), depois versionar cada produto afetado.
# 1. Muda o padrão na família — vale para todos os produtos abaixo dela
curl -s -X PATCH "$API/families/$FAMILY_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"defaultParams": { "iofDailyRate": 0.0000411, "gracePeriodMonths": 0 }}'
# 2. Lista os produtos da família
curl -s "$API/products?filter\[familyId\]=$FAMILY_ID&pageSize=100" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[].id'
# 3. Para cada um, nova versão + publish (o loop é seu)
Armadilhas.
PATCHna família não versiona nada sozinho. Os produtos passam a resolver com o valor novo imediatamente nas leituras ao vivo, mas as versões já criadas continuam com o snapshot antigo — que é o comportamento correto. Sem criar versões novas, a mudança nunca vira uma versão publicada.defaultParamsda família não é validado contra o schema da categoria. Sóparametersde produto eparameterOverridesde variante passam por Zod. Erro de digitação emdefaultParamsentra no banco e só aparece noresolvedParameters(§15).- Publique produto a produto. Não existe operação em lote;
POST /versionsé uma versão por chamada.
Descobrir por que a simulação reprovou
curl -s -X POST $API/simulate -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"productId\":\"$PRODUCT_ID\",\"context\":{\"renda\":4200,\"score\":720}}" \
| jq '.data.attributes.eligibility.results[] | select(.passed == false)'
Ordem de diagnóstico:
- A regra apareceu em
results? Se não, ela estáactive: false— regras inativas são puladas silenciosamente. actualValueveionull/ausente? Ofieldnão existe nocontextque você mandou. Campo ausente reprova.- Você passou
variantId? SemvariantId, a avaliação usa só as regras de produto (variantId = null). ComvariantId, usa as de produto mais as daquela variante. É diferente doGET /eligibility-rules?filter[variantId]=..., que devolve só as da variante. - Os tipos batem?
GTEcom string de um lado e número do outro reprova sem erro."4200"não é4200.
Consultar a condição vigente na data de um contrato
# Guarde o versionId no seu contrato no momento da originação
curl -s "$API/versions/latest?productId=$PRODUCT_ID" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data.id'
# Meses depois, reconstrua a condição exata
curl -s "$API/versions/$VERSION_ID_DO_CONTRATO" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.parameterSnapshot'
Armadilhas.
- Guarde o
versionId, não o número da versão. O número só é único dentro do par(productId, variantId). - Consulta por ID funciona em qualquer estado, inclusive
ARCHIVED. A listagem é que esconde arquivadas. - Rodar
POST /simulatecom oversionIddo contrato reproduz a condição da época, porque a resolução vem do snapshot e não das tabelas atuais.
12Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token; as 18 permissões PORTFOLIO_* e o organizationId vêm dele | Sim |
| Calculations Engine | Recebe os parâmetros resolvidos e calcula parcela, IOF e CET. Integração por contrato, não por código | Não |
| Pricing Engine | Define a taxa dentro da faixa minInterestRate–maxInterestRate do produto. Integração por contrato, não por código | Não |
| Decision Platform | Orquestra a decisão de crédito; consulta o catálogo para saber qual produto oferecer | Não |
| Decision Engine | Regra de crédito em DMN, quando campo-operador-valor não basta | Não |
| Products | Catálogo de produtos genérico, com preço e estoque. Building block diferente — este aqui é para produto financeiro | Não |
| Audit Trail | Consome os 22 eventos publicados para trilha de compliance | Não |
| Webhooks Engine | Entrega os eventos de versão a sistemas externos | Não |
Seja honesto na venda: hoje o Banking Product Portfolio não chama Pricing Engine nem Calculations Engine. Não há facade, não há
ModuleClient, não há import — oSimulationServicedepende apenas dos repositórios do próprio módulo, doParameterResolverServicee doEligibilityService. A composição da esteira é feita pelo orquestrador do cliente ou pelo Decision Platform, que chama cada building block em sequência. O diagrama abaixo mostra o desenho da esteira, não uma cadeia de chamadas automáticas dentro deste módulo.
Onde este bloco entra na esteira de crédito
┌──────────┐
│ Proposta │ cliente pede R$ 20.000 em 48x
└────┬─────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ BANKING PRODUCT PORTFOLIO │
│ POST /simulate │
│ → resolvedParameters: faixa de valor, faixa de taxa, prazo, IOF │
│ → eligibility: passou nos cortes de renda, idade, UF? │
│ → versionId: a versão vigente, que o contrato vai referenciar │
└────┬──────────────────────────────────────────────────────────────────┘
│ reprovou → devolve o nome da regra que barrou. Fim.
│ passou ↓
┌───────────────────────────────────────────────────────────────────────┐
│ DECISION PLATFORM + DECISION ENGINE │
│ bureau, score, política de crédito, tabela DMN → aprovado? limite? │
└────┬──────────────────────────────────────────────────────────────────┘
│ aprovado ↓
┌───────────────────────────────────────────────────────────────────────┐
│ PRICING ENGINE │
│ faixa de risco + regras → taxa final │
│ a taxa DEVE cair dentro de minInterestRate..maxInterestRate │
│ que veio do snapshot da versão ◀── o portfolio é o mandato │
└────┬──────────────────────────────────────────────────────────────────┘
│ taxa definida ↓
┌───────────────────────────────────────────────────────────────────────┐
│ CALCULATIONS ENGINE │
│ POST /calculations-engine/api/v1/calculations/simulation │
│ amortização (Price/SAC) · IOF (Decreto 6.306/2007) · CET │
│ entradas: valor, prazo, taxa do Pricing, iofDailyRate do snapshot │
└────┬──────────────────────────────────────────────────────────────────┘
│ parcela, CET e cronograma ↓
┌───────────────────────────────────────────────────────────────────────┐
│ CONTRATO → guarda o versionId. Para sempre. │
│ E-SIGNATURE assina · AUDIT TRAIL registra · BILLING cobra │
└───────────────────────────────────────────────────────────────────────┘
A composição acima é feita pelo orquestrador. Este building block é o
primeiro elo: define O QUE pode ser vendido e SOB QUAIS LIMITES.
Por que esse encadeamento é o argumento comercial. Cada peça sozinha é substituível. Junto, o encaixe é o produto: o maxInterestRate que o Portfolio congelou no snapshot é o teto que o Pricing Engine respeita; o iofDailyRate do mesmo snapshot é a entrada que o Calculations Engine usa para apurar o IOF; e o versionId que o contrato guarda é o que amarra os três de volta na hora da auditoria. Sem o catálogo versionado, o Pricing precifica sem mandato e o Calculations calcula sobre um parâmetro que pode ter mudado ontem.
13Configuração e operação
Variáveis de ambiente
Este building block não tem variável própria. Usa só a configuração compartilhada.
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATABASE_URL | PostgreSQL. O schema banking_portfolio precisa existir (criado pelas migrações) | Sim | — |
JWT_SECRET | Segredo HS256, mínimo 44 caracteres. Usado para verificar o token do IAM | Sim | — |
PORT | Porta no modo standalone | Não | 3000 (o registry define 3018 para este módulo) |
DEPLOYMENT_MODE | monolith ou standalone | Não | standalone (forçado em main.ts) |
MODULE_SELF | Identifica o serviço no /health | Não | banking-product-portfolio |
REDIS_URL | Redis compartilhado. Não usado por este módulo — nem cache nem rate limit próprio | Não | — |
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema banking_portfolio, cinco tabelas |
| IAM | Emissão do token. Verificação é local, sem chamada de rede |
Sem S3, sem Redis, sem fila, sem serviço externo. provedores: 0 no frontmatter é literal.
Limites
| Limite | Valor |
|---|---|
pageSize máximo na listagem | 100 (padrão 20) |
Tamanho do name | 255 caracteres |
Tamanho da description | 1.000 caracteres |
Formato do slug | Minúsculas, números e hífen, até 100 caracteres |
Formato da version | MAJOR.MINOR.PATCH, só dígitos |
Tamanho de parameters / parameterSnapshot | Sem limite na aplicação — limitado pelo JSONB do Postgres |
| Regras avaliadas por simulação | Sem limite; todas as regras ativas do produto são avaliadas em memória |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado no Zod, ou parâmetros inválidos para a categoria da família | A mensagem lista campo e motivo. Confira a tabela de parâmetros da §8 |
400 | VALIDATION | Invalid range: minX must be less than or equal to maxX | Inverteu min e max |
400 | VALIDATION | ...variants can only restrict ranges, not amplify them | A variante tentou abrir a faixa do produto-pai |
400 | VALIDATION | Invalid status transition: X -> Y | Consulte a máquina de estados da §8 |
400 | VALIDATION | Approver must be different from version creator | Aprove com outro usuário. Não há como desligar |
400 | VALIDATION | Variant does not belong to the informed product | O variantId é de outro produto |
400 | VALIDATION | Operator BETWEEN expects a two-item array [min, max] | Formato de value incompatível com o operador |
401 | UNAUTHORIZED | Token ausente, inválido ou expirado | Renove no IAM |
403 | FORBIDDEN | Falta a permissão PORTFOLIO_* exigida pela rota | Confira a §9 e as permissões contratadas pela organização |
403 | — | Organization context required | O token não tem organizationId. Autentique informando a organização |
404 | NOT_FOUND | Recurso inexistente, excluído logicamente, ou de outra organização | O 404 para recurso de outra organização é intencional |
409 | CONFLICT | slug já existe no escopo (organização, família ou produto) | Escolha outro slug |
409 | CONFLICT | Version is already published | A versão já está no estado que você pediu |
409 | CONFLICT | Cannot reactivate version because another published version exists | Deprecie ou suspenda a vigente antes |
500 | INTERNAL | Falha de banco ou de publicação de evento | Verifique PostgreSQL e o barramento de eventos |
Observabilidade.
GET /banking-product-portfolio/healthdevolve nome e versão do build. É uma sonda rasa — não testa o banco. Para orquestrador, complemente com uma verificação de conectividade com o PostgreSQL.- Vinte e dois tipos de evento são publicados pelo
EventPublisher, comorganizationId,userIdetimestampemmetadata. Os que importam para monitoramento de negócio:version.published,version.approved,version.rejected,version.suspendedesimulation.executed. - A publicação de evento é bloqueante e pode falhar a operação. Todos os serviços encadeiam o
publishnoResultAsync; se o barramento estiver fora, a chamada devolve500comFailed to publish event— mesmo com a escrita no banco já efetivada. Monitore o barramento com o mesmo cuidado que monitora o banco. - Não há métrica própria, nem cache, nem rate limit específico deste módulo.
14Segurança e compliance
Isolamento entre tenants. O organizationId vem do claim assinado do JWT e nunca do corpo da requisição. As 34 rotas aplicam requireOrganization, que devolve 403 quando o claim está ausente. Na camada de dados, todo método de repositório recebe organizationId e o usa no where — findById é findFirst({ where: { id, organizationId, deletedAt: null } }), não findUnique({ where: { id } }). Consultar um recurso de outra organização pelo ID devolve 404, não 403: a distinção permitiria enumerar recursos alheios.
Autorização. São 18 permissões PORTFOLIO_*, definidas no vocabulário compartilhado do IAM. A separação entre PORTFOLIO_VERSION_PUBLISH (publicar, depreciar, arquivar) e PORTFOLIO_APPROVE (submeter, aprovar, rejeitar, suspender, reativar) existe justamente para permitir segregação de funções na atribuição de papéis.
Segregação de funções na aprovação. approveVersion compara version.createdBy com o userId do token e recusa quando são iguais. Não é configuração, é código. approvedBy e approvedAt ficam gravados na linha da versão.
Dados sensíveis. Este building block não guarda dado pessoal. Ele guarda parâmetros de produto — valores, taxas, prazos — e regras de elegibilidade. O context da simulação, que pode conter renda, idade e score de uma pessoa, não é persistido: é avaliado em memória e descartado. O evento simulation.executed publica apenas productId, variantId, versionId, o booleano eligible e a contagem de regras avaliadas — nunca o conteúdo do contexto. Do ponto de vista de LGPD, a superfície é mínima por construção.
Exclusão lógica em tudo. As cinco tabelas usam deletedAt. Nada é apagado fisicamente. É o comportamento exigido para retenção de registro de produto financeiro, e é a razão de o DELETE devolver 204 sem remover linha.
Imutabilidade da versão. VersionRepository.update só altera status, publishedAt, publishedBy, approvedBy e approvedAt. Não existe caminho de código que reescreva parameterSnapshot depois da criação. É essa propriedade que sustenta a resposta a auditoria.
Enquadramento regulatório — leia com atenção. Este building block armazena parâmetros; ele não calcula, não apura e não declara nada ao regulador. Especificamente:
- CET. A Resolução CMN 4.881/2020, em vigor desde 1º de fevereiro de 2021 e que revogou a Resolução CMN 3.517/2007, disciplina o cálculo e a divulgação do Custo Efetivo Total nas operações de crédito e arrendamento mercantil. Este módulo não calcula CET. Quem calcula é o Calculations Engine. Aqui ficam apenas os parâmetros que alimentam esse cálculo — taxa, tarifa de cadastro, seguro, IOF.
- IOF. O Decreto 6.306/2007 regulamenta o IOF, e foi alterado em 2025 pelos Decretos 12.466 e 12.467. O campo
iofDailyRateé um número que você informa — a aplicação não valida se ele corresponde à alíquota vigente, e não há atualização automática quando a norma muda. Manter a alíquota correta é responsabilidade da instituição. - Consignado. A Lei 10.820/2003, alterada pela MP 1.292/2025 e convertida na Lei 15.179/2025, regula o crédito consignado, incluindo o modelo para trabalhador CLT com contratação pela CTPS Digital. Nada disso é implementado aqui. Averbação, margem consignável e integração com a plataforma Crédito do Trabalhador da DATAPREV estão fora do escopo. O que este módulo oferece é a modelagem de um produto de consignado — nome, faixas, prazos, regras de corte.
Em resumo, para uso comercial: não afirme que este building block é "aderente ao BACEN" ou que "atende à Resolução 4.881". Ele não é e não atende, porque não é ele quem faz o que a norma exige. O que ele faz, e é bastante, é dar rastreabilidade versionada aos parâmetros que os módulos de cálculo consomem — o que ajuda a instituição a demonstrar conformidade, sem ser conformidade.
Nada de segredo. Este módulo não guarda credencial, chave nem token. Não há campo criptografado porque não há dado que justifique criptografia em repouso além da do próprio banco.
15Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
| Sem cálculo financeiro na simulação | POST /simulate devolve parâmetros e elegibilidade. Não devolve parcela, IOF apurado, CET nem cronograma. Quem espera uma simulação de crédito completa vai se frustrar | Por design — é o Calculations Engine. A composição é do orquestrador |
| Sem integração com Pricing Engine, Calculations Engine ou Decision Platform | Não há facade nem chamada HTTP para nenhum outro building block. A esteira da §12 é desenho de arquitetura, não código deste módulo | Não implementado. Era não-goal declarado no design original |
| Elegibilidade não chama Decision Platform | Regra complexa (bureau, árvore, DMN) não roda aqui. Só campo-operador-valor sobre um contexto que você monta | Por design. O campo decisionConfigKey foi previsto no design e não existe no schema |
priority da regra não faz nada além de ordenar | Todas as regras ativas são avaliadas e todas precisam passar. Não há curto-circuito, não há peso, não há regra "bloqueante" versus "informativa" | Não implementado |
filter[active]=false se comporta como true em famílias e produtos | Os dois routers usam z.coerce.boolean(), e Boolean("false") é true. Filtrar por inativos não funciona nessas duas rotas. O router de variantes trata corretamente | Bug conhecido. Contorne listando tudo e filtrando no cliente |
defaultParams da família não é validado | Só parameters de produto e parameterOverrides de variante passam por Zod. Chave errada ou tipo errado em defaultParams entra no banco e só aparece no resolvedParameters | Não implementado |
versions/latest ordena por publishedAt, não por semver | Publicar a 1.0.1 depois da 2.0.0 faz da 1.0.1 a "latest". O campo version é validado como semver mas nunca comparado como tal | Por design (vale o publicado por último), mas surpreende |
SUSPENDED não vai direto para DEPRECATED | Para depreciar uma versão suspensa, é preciso reativar antes. SUSPENDED só transiciona para PUBLISHED | Limitação da máquina de estados |
DRAFT não pode ser arquivado nem excluído | Só DEPRECATED arquiva, e não há DELETE de versão. Rascunhos abandonados ficam no banco para sempre | Não implementado |
| Excluir produto ou família não checa dependentes | Você consegue excluir logicamente um produto com versões publicadas e variantes ativas. As versões permanecem consultáveis — desejável para o contrato antigo, surpreendente para quem espera integridade referencial | Por design, mas sem aviso na API |
| Sem operação em lote | Uma mudança regulatória que afeta 40 produtos exige 40 chamadas de criação e 40 de publicação | Não implementado |
| Sem comparação entre versões | Não há endpoint que devolva o diff entre duas versões. O consumidor busca as duas e compara | Não implementado |
| Sem agendamento de vigência | A versão entra em vigor no instante da chamada de publish. Não dá para agendar "publica dia 1º" | Não implementado |
| A publicação de evento pode derrubar a operação | Se o EventPublisher falhar, a chamada devolve 500 mesmo com a escrita já efetivada no banco. O cliente pode concluir que falhou quando não falhou | Comportamento herdado do padrão do projeto |
| Health check raso | /health não testa PostgreSQL. Um serviço com banco inacessível continua respondendo 200 | Não implementado |
| Sem host em staging | O building block não está na lista de serviços publicados em AMBIENTES.md. O teste é local ou em monolito | Pendência de infraestrutura |
| Sem testes E2E | Há testes unitários e de integração dos serviços. Não há suíte E2E das 34 rotas HTTP | Roadmap |
| Rota com o nome do módulo duplicado | O caminho real é /banking-product-portfolio/api/v1/banking-product-portfolio/..., e os links.self das respostas omitem o basePath — não são navegáveis diretamente | Corrigir quebraria integração existente |
16Perguntas frequentes
Qual a diferença entre este building block e o Products?
O Products é um catálogo de produtos comerciais — item, preço, estoque. Este é um catálogo de produtos financeiros: faixa de valor, faixa de taxa, prazo, IOF, elegibilidade, versão. Os conceitos centrais não se sobrepõem: no Products não existe "versão sob a qual o contrato foi assinado", e aqui não existe estoque. Se a sua operação vende crédito, é este. Se vende mercadoria, é o outro.
Preciso trocar meu core banking para usar isso?
Não. Este building block tem banco próprio no schema banking_portfolio, fala HTTP e não escritura nada — não abre conta, não movimenta saldo, não gera lançamento contábil. Ele fica ao lado do core que você já opera. É justamente a diferença em relação a Mambu, Vault Core e Temenos, que entregam o catálogo dentro de um core completo.
Se eu alterar o produto, o que acontece com os contratos já assinados?
Nada, desde que o contrato guarde o versionId. A versão publicada carrega um parameterSnapshot congelado no momento em que foi criada, e nenhum caminho de código reescreve esse JSON. PATCH no produto muda o que vale daqui para a frente; GET /versions/:versionId continua devolvendo a condição de antes. Se o seu contrato não guarda o versionId, essa garantia não existe — guarde-o na originação.
Por que a versão congela na criação e não na publicação?
Porque o rascunho precisa ser revisável. Se o snapshot fosse tirado na publicação, o aprovador estaria aprovando um conteúdo que poderia mudar entre a aprovação e a publicação — o que anula a aprovação. O efeito colateral é que alterar o produto depois de criar o rascunho não entra na versão: nesse caso, crie outra versão.
Dá para pular a revisão e publicar direto?
Dá. publish aceita origem DRAFT. Se a sua operação exige revisão, o controle é por permissão, não por estado: dê PORTFOLIO_VERSION_PUBLISH só a quem publica depois de aprovar, e PORTFOLIO_APPROVE a quem revisa. As duas permissões são separadas exatamente para isso.
A simulação me devolve a parcela do empréstimo?
Não. Ela devolve os parâmetros resolvidos e o resultado da elegibilidade, regra a regra. Parcela, IOF apurado, CET e cronograma de amortização são do Calculations Engine, que você chama em seguida com os parâmetros que este módulo devolveu. Fluxo completo na §12.
Este building block me deixa em conformidade com o BACEN?
Não, e é importante ser preciso. Ele armazena parâmetros — inclusive iofDailyRate e a tarifa de cadastro, que entram no cálculo de CET. Ele não calcula CET, não apura IOF, não valida alíquota contra a norma vigente e não envia nada ao regulador. O que ele entrega é rastreabilidade versionada dos parâmetros, o que ajuda a demonstrar conformidade sem ser conformidade. Detalhe das normas na §14.
Como modelo a tabela de um correspondente que negociou condição própria?
Como uma variante do produto, com parameterOverrides contendo só o que difere. A variante herda o resto por deep merge e só consegue restringir faixas — tentar prazo maior que o do produto-pai devolve 400. Depois crie uma versão do par produto/variante para congelar a condição. Receita completa na §11.
Uma regra de elegibilidade consegue consultar bureau de crédito?
Não. A avaliação é in-process, sobre o context que você envia — sem chamada de rede. Você busca o score no bureau, coloca no context e a regra compara. Regra que precisa orquestrar consulta externa é caso do Decision Platform, e a integração entre os dois ainda não existe (§15).
Quantos produtos e versões esse módulo aguenta?
Não há limite na aplicação, e não temos número de carga medido para publicar. As consultas são indexadas por organizationId, productId, variantId, status e deletedAt, e a listagem é paginada com teto de 100 itens. O ponto de atenção é armazenamento: cada versão guarda o snapshot completo em JSONB, e o crescimento é linear no número de versões. O estado ARCHIVED tira as antigas da operação sem apagá-las.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md
Building blocks relacionados
Taxa por faixa de risco, com tarifas, comissões e seguros no mesmo cálculo
Decision PlatformA esteira que busca os dados, aplica a política e devolve o veredito
Decision EngineRegras de negócio em tabelas DMN, versionadas e alteradas sem deploy
IAMIdentidade, organizações e permissões para todo o catálogo de building blocks