Catalisa.
Building blocks/FinanceiroBeta

Banking Product Portfolio

Catálogo de produtos financeiros com versão congelada em cada contrato

34
Endpoints
5
Entidades
0
Provedores
Tenant
Escopo
3018
Porta

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.

Para quem é
  • 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
Substitui
  • 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
O que não é
  • 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.

AtributoValor
Identificadorbanking-product-portfolio
CategoriaFinanceiro
EscopoTenant (exige organizationId no token em todas as 34 rotas)
Porta (standalone)3018
Path alias@banking-product-portfolio
Prefixo HTTP/banking-product-portfolio
Schema PostgreSQLbanking_portfolio
StatusBeta desde 2026-02
Depende dePostgreSQL, 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

AntesDepois
Alterar teto de prazo é ticket, sprint e deployPATCH no produto, nova versão, publicar — sem release
Sobrescrever a taxa apaga a condição anteriorA versão antiga vira DEPRECATED e continua consultável para sempre
Cada canal duplica o produto inteiro para mudar dois camposVariante declara só o que muda e herda o resto
"Quem autorizou essa taxa?" é uma conversaapprovedBy e approvedAt gravados, com aprovador ≠ criador exigido pelo código
Elegibilidade é if espalhado na esteiraRegra 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érioCatalisa PortfolioMambuThought Machine Vault CoreTemenos TransactMatera
EscopoSó o catálogo de produtoCore banking completoCore banking completoCore banking completoCore banking completo
Ledger e escrituração de contratoNão temSimSimSimSim
Definição de produtoJSONB + Zod, por APIConfiguração no product engineSmart contract em PythonConfiguração do TransactConfiguração + APIs
Versão imutável de produtoSim, snapshot por versãoSimSim, com versionamento de códigoDepende da implantaçãoNão confirmado publicamente
Aprovação com segregação de funçõesSim, no códigoVia workflow configurávelVia pipeline de códigoVia workflowNão confirmado publicamente
Herança família → produto → varianteSim, deep merge com restriçãoProduct templatesComposição de contratosHierarquia de produtoNão confirmado publicamente
Precificação por riscoNão (é o Pricing Engine)ParcialVia smart contractSimParcial
Cálculo de parcela, IOF e CETNão (é o Calculations Engine)SimSim, no smart contractSimSim
PreçoPrecificação em definiçãoNão publicadoNão publicadoNão publicadoNão publicado
Porte de entradaBuilding block avulsoProjeto de coreProjeto de bancoProjeto de bancoProjeto 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

  1. 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 PATCH no 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.
  2. A segregação de funções é uma linha de código, não uma promessa de processo. approveVersion compara version.createdBy com o userId do token e devolve erro quando são iguais. Não há configuração que desligue isso.
  3. A variante restringe, nunca amplia. validateRangeRestrictions compara cada min/max do 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.
  4. É 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:

DriverPor que é justo
Produtos ativos no catálogoMede o tamanho da operação, não o número de usuários que a operam
Versões publicadas por mêsMede 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 PortfolioCore banking completo (qualquer um dos quatro)
Custo de licençaPrecificação em definiçãoContrato negociado, não publicado
Projeto de implantaçãoIntegração de APIProjeto de meses a anos, com migração de dados
Substituição do core atualNão exigidaExigida, ou operação em dual-core
O que você ganha juntoSó o catálogoLedger, 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 tem waitingPeriodDays. Modelar em colunas exigiria migração a cada categoria nova e deixaria a tabela cheia de NULL. O trade-off aceito é que o banco não valida nada: a garantia de tipo vem do Zod na camada de serviço, e um INSERT feito 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. createVersion resolve família → produto → variante e grava o resultado em parameterSnapshot. VersionRepository.update nunca toca esse campo — só mexe em status, 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.

  • ARCHIVED como terminal oculto, separado de DEPRECATED. DEPRECATED significa "não vendo mais, mas ainda sustento" e continua aparecendo na listagem. ARCHIVED some da listagem padrão e só volta com filter[includeArchived]=true ou 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. findLatestPublished ordena por publishedAt: 'desc'. O campo version é validado como semver (\d+\.\d+\.\d+) mas nunca é comparado como semver. Publicar a 1.0.1 depois da 2.0.0 faz da 1.0.1 a 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êm deletedAt, e todo repositório filtra deletedAt: 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

TermoSignifica
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 resolvidosO resultado do deep merge família → produto → variante. Aparece como resolvedParameters nas respostas de produto e variante, e como parameterSnapshot na versão.
CategoriaCREDIT, DEPOSIT, TRANSACTION, INVESTMENT, INSURANCE. Determina o schema de validação dos parâmetros.
SegmentoRETAIL, 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 PrismaTabelaPropósitoCampos-chave
BankingProductFamilybanking_product_familiesAgrupa por categoria e segmento; define padrõescategory, segment, defaultParams (JSONB), único (organizationId, slug)
BankingProductbanking_productsProduto concreto com parâmetrosfamilyId, productType, parameters (JSONB), único (organizationId, familyId, slug)
BankingProductVariantbanking_product_variantsVariação com overrides parciaisproductId, parameterOverrides (JSONB), único (organizationId, productId, slug)
PortfolioVersionportfolio_versionsSnapshot imutável versionadoversion, status, parameterSnapshot (JSONB), publishedAt/By, approvedAt/By, único (productId, variantId, version)
EligibilityRuleeligibility_rulesRegra campo-operador-valorfield, operator, value (JSONB), priority, active

Enumerações — modeladas como String no Prisma e validadas por Zod na aplicação.

EnumeraçãoValores
ProductCategoryCREDIT · DEPOSIT · TRANSACTION · INVESTMENT · INSURANCE
CustomerSegmentRETAIL · CORPORATE · PRIVATE · SME
PortfolioVersionStatusDRAFT · REVIEW · APPROVED · PUBLISHED · DEPRECATED · SUSPENDED · ARCHIVED
EligibilityOperatorEQ · NEQ · GT · GTE · LT · LTE · IN · NOT_IN · BETWEEN · REGEX

Parâmetros por categoria. Cada categoria tem seu schema. Obrigatórios em negrito.

CategoriaCampos
CREDITminAmount, maxAmount, minInterestRate, maxInterestRate, minTerm, maxTerm, gracePeriodMonths, iofDailyRate, iofAdditionalRate, insuranceRate, registrationTariffRate, amortizationMethods[]
DEPOSITminAmount, maxAmount, minTerm, maxTerm, interestRate, indexer, indexerPercentage, earlyWithdrawalPenaltyRate
TRANSACTIONmonthlyFee, maintenanceFee, freeTransfersPerMonth, transferFee, freeWithdrawalsPerMonth, withdrawalFee, pixLimitPerTransaction, pixDailyLimit
INVESTMENTminAmount, maxAmount, managementFeeRate, performanceFeeRate, benchmarkIndex, liquidityDays, riskLevel
INSURANCEminPremium, 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étodoRotaDescriçãoPermissão
POST/banking-product-portfolio/api/v1/banking-product-portfolio/familiesCria famíliaPORTFOLIO_FAMILY_CREATE
GET/banking-product-portfolio/api/v1/banking-product-portfolio/familiesLista famílias, paginadoPORTFOLIO_FAMILY_READ
GET/banking-product-portfolio/api/v1/banking-product-portfolio/families/:familyIdBusca famíliaPORTFOLIO_FAMILY_READ
PATCH/banking-product-portfolio/api/v1/banking-product-portfolio/families/:familyIdAtualiza famíliaPORTFOLIO_FAMILY_UPDATE
DELETE/banking-product-portfolio/api/v1/banking-product-portfolio/families/:familyIdExclusão lógica. 204PORTFOLIO_FAMILY_DELETE

Filtros da listagem: filter[category], filter[segment], filter[active]. Paginação: pageNumber, pageSize (máx. 100, padrão 20).

Produtos — /products

MétodoRotaDescriçãoPermissão
POST/banking-product-portfolio/api/v1/banking-product-portfolio/productsCria produtoPORTFOLIO_PRODUCT_CREATE
GET/banking-product-portfolio/api/v1/banking-product-portfolio/productsLista produtos, com resolvedParametersPORTFOLIO_PRODUCT_READ
GET/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productIdBusca produto, com resolvedParametersPORTFOLIO_PRODUCT_READ
PATCH/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productIdAtualiza produtoPORTFOLIO_PRODUCT_UPDATE
DELETE/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productIdExclusão lógica. 204PORTFOLIO_PRODUCT_DELETE

Filtros: filter[familyId], filter[category] (via família), filter[active].

Variantes — /products/:productId/variants

MétodoRotaDescriçãoPermissão
POST/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variantsCria variantePORTFOLIO_VARIANT_CREATE
GET/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variantsLista variantes, com resolvedParametersPORTFOLIO_VARIANT_READ
GET/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants/:variantIdBusca variantePORTFOLIO_VARIANT_READ
PATCH/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants/:variantIdAtualiza variantePORTFOLIO_VARIANT_UPDATE
DELETE/banking-product-portfolio/api/v1/banking-product-portfolio/products/:productId/variants/:variantIdExclusão lógica. 204PORTFOLIO_VARIANT_DELETE

Filtro: filter[active].

Versões — /versions

MétodoRotaDescriçãoPermissão
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versionsCria versão em DRAFT e congela o snapshotPORTFOLIO_VERSION_CREATE
GET/banking-product-portfolio/api/v1/banking-product-portfolio/versionsLista versões, paginadoPORTFOLIO_VERSION_READ
GET/banking-product-portfolio/api/v1/banking-product-portfolio/versions/latestÚltima versão publicada de um par produto/variantePORTFOLIO_VERSION_READ
GET/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionIdBusca versão pelo IDPORTFOLIO_VERSION_READ
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/publishPublica e deprecia a anteriorPORTFOLIO_VERSION_PUBLISH
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/deprecateMarca como DEPRECATEDPORTFOLIO_VERSION_PUBLISH
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/archiveMarca como ARCHIVEDPORTFOLIO_VERSION_PUBLISH
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/submitEnvia para revisãoPORTFOLIO_APPROVE
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/approveAprova. Aprovador ≠ criadorPORTFOLIO_APPROVE
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/rejectRejeita e volta para DRAFTPORTFOLIO_APPROVE
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/suspendSuspende versão publicadaPORTFOLIO_APPROVE
POST/banking-product-portfolio/api/v1/banking-product-portfolio/versions/:versionId/reactivateReativa versão suspensaPORTFOLIO_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, suspend e reactivate usam PORTFOLIO_APPROVE; publish, deprecate e archive usam PORTFOLIO_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étodoRotaDescriçãoPermissão
POST/banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rulesCria regraPORTFOLIO_ELIGIBILITY_MANAGE
GET/banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rulesLista regras de um produto. filter[productId] obrigatórioPORTFOLIO_ELIGIBILITY_MANAGE
GET/banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/:ruleIdBusca regraPORTFOLIO_ELIGIBILITY_MANAGE
PATCH/banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/:ruleIdAtualiza regraPORTFOLIO_ELIGIBILITY_MANAGE
DELETE/banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/:ruleIdExclusão lógica. 204PORTFOLIO_ELIGIBILITY_MANAGE
POST/banking-product-portfolio/api/v1/banking-product-portfolio/eligibility-rules/check-eligibilityAvalia um contexto contra as regrasPORTFOLIO_ELIGIBILITY_MANAGE

Simulação — /simulate

MétodoRotaDescriçãoPermissão
POST/banking-product-portfolio/api/v1/banking-product-portfolio/simulateResolve parâmetros e avalia elegibilidadePORTFOLIO_SIMULATE

Saúde

MétodoRotaDescrição
GET/banking-product-portfolio/healthIdentificaçã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"
}
CampoTipoObrigatórioDescrição
versionstringSimSemver MAJOR.MINOR.PATCH. Formato validado, ordem não comparada
productIdstring (UUID)SimProduto
variantIdstring (UUID)NãoOmitido, 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

StatusQuando
400version fora do formato semver, ou variante que não pertence ao produto informado
403Falta PORTFOLIO_VERSION_CREATE, ou token sem organizationId
404Produto, variante ou família inexistente na sua organização
409Já 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

StatusQuando
400Transição não permitida (por exemplo, de DEPRECATED ou ARCHIVED)
409A versão já está PUBLISHED
404Versão inexistente na sua organização

POST .../versions/:versionId/approve

Aprova uma versão em REVIEW. Grava approvedBy e approvedAt.

Erros

StatusQuando
400Versão não está em REVIEW
400O aprovador é o mesmo usuário que criou a versãoApprover must be different from version creator
409A versão já está APPROVED

GET .../versions/latest

ParâmetroTipoObrigatórioDescrição
productIdUUID em query stringSimOmitido, retorna 400
variantIdUUID em query stringNãoOmitido, 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 }
}
CampoTipoObrigatórioDescrição
productIdUUIDSimProduto
variantIdUUIDNãoVariante
versionIdUUIDNãoCom ele, os parâmetros vêm do parameterSnapshot da versão. Sem ele, são resolvidos ao vivo das três camadas
contextobjetoNão (padrão {})Os dados avaliados pelas regras. Aceita caminho com ponto: uma regra com field: "cliente.renda"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

StatusQuando
400Variante não pertence ao produto, ou versão não corresponde ao par produto/variante informado
404Produto, 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
}
CampoTipoObrigatórioDescrição
productIdUUIDSimVai no corpo, não na rota
variantIdUUIDNãoRegra específica de uma variante
namestring (1–255)SimAparece no resultado da simulação — escreva pensando em quem vai ler
fieldstring (1–255)SimChave do contexto. Aceita caminho com ponto
operatorenumSimVer tabela abaixo
valuequalquerSimO tipo depende do operador
priorityinteiro ≥ 0Não (0)Só ordena o resultado. Não interrompe a avaliação nem pondera
activebooleanoNão (true)Regra inativa é ignorada e não aparece em results
OperadorFormato do valueComportamento
EQ / NEQqualquerIgualdade estrutural profunda
GT / GTE / LT / LTEnúmero ou stringComparação; tipos diferentes reprovam
IN / NOT_INarrayPertinência ao array
BETWEENarray de 2 itens comparáveisInclusivo nas duas pontas
REGEXstring, ou { "pattern": "...", "flags": "i" }Só avalia contra string

Campo ausente no contexto reprova a regra. Se o field não existe no context, passed vem false e actualValue vem undefined. 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_A devolve 400 com Approver must be different from version creator. Não há como desligar.
  • submit, approve e reject exigem PORTFOLIO_APPROVE; publish exige PORTFOLIO_VERSION_PUBLISH. São permissões diferentes. Um usuário só de aprovação não publica.
  • Não é preciso passar por revisão: publish aceita DRAFT direto. Se o seu controle exige revisão, restrinja PORTFOLIO_VERSION_PUBLISH a 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 com maxTerm: 84 devolve 400, 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.0 do produto sem variante e 1.0.0 do par produto/variante são duas linhas distintas — a chave única é (productId, variantId, version). GET /versions/latest sem variantId busca 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.

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.

  • PATCH na 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.
  • defaultParams da família não é validado contra o schema da categoria.parameters de produto e parameterOverrides de variante passam por Zod. Erro de digitação em defaultParams entra no banco e só aparece no resolvedParameters (§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:

  1. A regra apareceu em results? Se não, ela está active: false — regras inativas são puladas silenciosamente.
  2. actualValue veio null/ausente? O field não existe no context que você mandou. Campo ausente reprova.
  3. Você passou variantId? Sem variantId, a avaliação usa as regras de produto (variantId = null). Com variantId, usa as de produto mais as daquela variante. É diferente do GET /eligibility-rules?filter[variantId]=..., que devolve as da variante.
  4. Os tipos batem? GTE com 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 /simulate com o versionId do 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 blockComo se relacionaObrigatório
IAMEmite o token; as 18 permissões PORTFOLIO_* e o organizationId vêm deleSim
Calculations EngineRecebe os parâmetros resolvidos e calcula parcela, IOF e CET. Integração por contrato, não por códigoNão
Pricing EngineDefine a taxa dentro da faixa minInterestRatemaxInterestRate do produto. Integração por contrato, não por códigoNão
Decision PlatformOrquestra a decisão de crédito; consulta o catálogo para saber qual produto oferecerNão
Decision EngineRegra de crédito em DMN, quando campo-operador-valor não bastaNão
ProductsCatálogo de produtos genérico, com preço e estoque. Building block diferente — este aqui é para produto financeiroNão
Audit TrailConsome os 22 eventos publicados para trilha de complianceNão
Webhooks EngineEntrega os eventos de versão a sistemas externosNã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 — o SimulationService depende apenas dos repositórios do próprio módulo, do ParameterResolverService e do EligibilityService. 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ávelDescriçãoObrigatóriaPadrão
DATABASE_URLPostgreSQL. O schema banking_portfolio precisa existir (criado pelas migrações)Sim
JWT_SECRETSegredo HS256, mínimo 44 caracteres. Usado para verificar o token do IAMSim
PORTPorta no modo standaloneNão3000 (o registry define 3018 para este módulo)
DEPLOYMENT_MODEmonolith ou standaloneNãostandalone (forçado em main.ts)
MODULE_SELFIdentifica o serviço no /healthNãobanking-product-portfolio
REDIS_URLRedis compartilhado. Não usado por este módulo — nem cache nem rate limit próprioNão

Dependências de infraestrutura

DependênciaPara quê
PostgreSQLSchema banking_portfolio, cinco tabelas
IAMEmissã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

LimiteValor
pageSize máximo na listagem100 (padrão 20)
Tamanho do name255 caracteres
Tamanho da description1.000 caracteres
Formato do slugMinúsculas, números e hífen, até 100 caracteres
Formato da versionMAJOR.MINOR.PATCH, só dígitos
Tamanho de parameters / parameterSnapshotSem limite na aplicação — limitado pelo JSONB do Postgres
Regras avaliadas por simulaçãoSem limite; todas as regras ativas do produto são avaliadas em memória

Catálogo de erros

StatusCódigoSignificaO que fazer
400VALIDATIONCorpo reprovado no Zod, ou parâmetros inválidos para a categoria da famíliaA mensagem lista campo e motivo. Confira a tabela de parâmetros da §8
400VALIDATIONInvalid range: minX must be less than or equal to maxXInverteu min e max
400VALIDATION...variants can only restrict ranges, not amplify themA variante tentou abrir a faixa do produto-pai
400VALIDATIONInvalid status transition: X -> YConsulte a máquina de estados da §8
400VALIDATIONApprover must be different from version creatorAprove com outro usuário. Não há como desligar
400VALIDATIONVariant does not belong to the informed productO variantId é de outro produto
400VALIDATIONOperator BETWEEN expects a two-item array [min, max]Formato de value incompatível com o operador
401UNAUTHORIZEDToken ausente, inválido ou expiradoRenove no IAM
403FORBIDDENFalta a permissão PORTFOLIO_* exigida pela rotaConfira a §9 e as permissões contratadas pela organização
403Organization context requiredO token não tem organizationId. Autentique informando a organização
404NOT_FOUNDRecurso inexistente, excluído logicamente, ou de outra organizaçãoO 404 para recurso de outra organização é intencional
409CONFLICTslug já existe no escopo (organização, família ou produto)Escolha outro slug
409CONFLICTVersion is already publishedA versão já está no estado que você pediu
409CONFLICTCannot reactivate version because another published version existsDeprecie ou suspenda a vigente antes
500INTERNALFalha de banco ou de publicação de eventoVerifique PostgreSQL e o barramento de eventos

Observabilidade.

  • GET /banking-product-portfolio/health devolve 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, com organizationId, userId e timestamp em metadata. Os que importam para monitoramento de negócio: version.published, version.approved, version.rejected, version.suspended e simulation.executed.
  • A publicação de evento é bloqueante e pode falhar a operação. Todos os serviços encadeiam o publish no ResultAsync; se o barramento estiver fora, a chamada devolve 500 com Failed 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 wherefindById é 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çãoImpactoSituação
Sem cálculo financeiro na simulaçãoPOST /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 frustrarPor design — é o Calculations Engine. A composição é do orquestrador
Sem integração com Pricing Engine, Calculations Engine ou Decision PlatformNão há facade nem chamada HTTP para nenhum outro building block. A esteira da §12 é desenho de arquitetura, não código deste móduloNão implementado. Era não-goal declarado no design original
Elegibilidade não chama Decision PlatformRegra complexa (bureau, árvore, DMN) não roda aqui. Só campo-operador-valor sobre um contexto que você montaPor design. O campo decisionConfigKey foi previsto no design e não existe no schema
priority da regra não faz nada além de ordenarTodas 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 produtosOs dois routers usam z.coerce.boolean(), e Boolean("false") é true. Filtrar por inativos não funciona nessas duas rotas. O router de variantes trata corretamenteBug conhecido. Contorne listando tudo e filtrando no cliente
defaultParams da família não é validadoparameters de produto e parameterOverrides de variante passam por Zod. Chave errada ou tipo errado em defaultParams entra no banco e só aparece no resolvedParametersNão implementado
versions/latest ordena por publishedAt, não por semverPublicar 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 talPor design (vale o publicado por último), mas surpreende
SUSPENDED não vai direto para DEPRECATEDPara depreciar uma versão suspensa, é preciso reativar antes. SUSPENDED só transiciona para PUBLISHEDLimitação da máquina de estados
DRAFT não pode ser arquivado nem excluídoDEPRECATED arquiva, e não há DELETE de versão. Rascunhos abandonados ficam no banco para sempreNão implementado
Excluir produto ou família não checa dependentesVocê 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 referencialPor design, mas sem aviso na API
Sem operação em loteUma mudança regulatória que afeta 40 produtos exige 40 chamadas de criação e 40 de publicaçãoNão implementado
Sem comparação entre versõesNão há endpoint que devolva o diff entre duas versões. O consumidor busca as duas e comparaNão implementado
Sem agendamento de vigênciaA 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çãoSe o EventPublisher falhar, a chamada devolve 500 mesmo com a escrita já efetivada no banco. O cliente pode concluir que falhou quando não falhouComportamento herdado do padrão do projeto
Health check raso/health não testa PostgreSQL. Um serviço com banco inacessível continua respondendo 200Não implementado
Sem host em stagingO building block não está na lista de serviços publicados em AMBIENTES.md. O teste é local ou em monolitoPendência de infraestrutura
Sem testes E2EHá testes unitários e de integração dos serviços. Não há suíte E2E das 34 rotas HTTPRoadmap
Rota com o nome do módulo duplicadoO 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 diretamenteCorrigir 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