Catalisa.Building Blocks
Catálogo/Financeiro/Products

Products

Produção

Parâmetros do seu produto de crédito por API, sem constante escondida no código

5
Endpoints
1
Entidades
0
Provedores
Tenant
Escopo
3004
Porta
2026-02
Desde

Faixa de valor, faixa de taxa, prazo, IOF e método de amortização do seu produto de crédito viram registro consultável por API — o time de produto muda o parâmetro sem abrir ticket, e a esteira lê sempre o valor vigente.

Para quem é
  • Fintechs de crédito que operam poucos produtos e não querem um catálogo bancário completo
  • Times de originação que hoje guardam faixa de taxa e prazo em constante dentro do código da esteira
  • Operações que precisam alimentar simulador, motor de decisão e calculadora com o mesmo parâmetro
Substitui
  • Constante de faixa de valor, taxa e prazo codificada dentro da esteira de originação
  • Planilha de parâmetros de produto compartilhada entre produto, risco e TI
O que não é
  • Um catálogo bancário versionado — quem congela a versão vigente em cada contrato é o banking-product-portfolio
  • Um catálogo de e-commerce com SKU, estoque e preço de venda — isso é o building block commerce
  • 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
O que dá para fazer

6 endpoints em 2 recursos.

Explorar a API →
01

Resumo executivo

O Products guarda o que a sua operação de crédito vende: de quanto a quanto, por qual taxa, em quantas parcelas, com qual seguro, qual tarifa de cadastro, quais alíquotas de IOF e por qual método de amortização. Cada produto é um registro consultável, e as faixas que a esteira usa saem dele em vez de saírem de uma constante no código.

Na prática ele resolve a conversa que se repete todo mês: o time comercial quer subir o teto de prazo de 48 para 60 parcelas, e a resposta é "entra na fila do desenvolvimento". Aqui é um PATCH — melhor, é um produto novo ou uma alteração de campo, feita por quem tem a permissão, sem janela de release.

Está em produção, publicado em staging e em produção como serviço próprio (products.bb.stg.catalisa.app), com testes unitários e de integração. É pequeno de propósito: cinco rotas, uma tabela. Se você precisa de família de produto, variante por canal, versão publicada e aprovação formal, este não é o building block certo — é o Banking Product Portfolio. A fronteira entre os dois está detalhada na §12 e vale ler antes de escolher.

Antes de tudo: é este building block que você quer?

A Catalisa tem três building blocks que guardam algo chamado "produto", e escolher errado custa uma migração. Esta é a dúvida número um de quem chega ao catálogo, então ela vem antes de qualquer outra coisa. A versão detalhada, com a tabela critério a critério, está na §12.

flowchart TD
    Q1{"O que você vende?"}
    Q1 -->|Mercadoria| CM["<b>commerce</b><br/>SKU, estoque, preço de venda,<br/>carrinho e pedido<br/>32 entidades · 130 rotas"]
    Q1 -->|Crédito| Q2{"Você precisa reconstruir, no futuro,<br/>sob qual condição cada contrato foi assinado?"}
    Q2 -->|Sim| BPP["<b>banking-product-portfolio</b><br/>família, variante por canal, versão congelada,<br/>aprovação formal e regra de elegibilidade<br/>5 entidades · 34 rotas"]
    Q2 -->|Não| PR["<b>products</b> — este building block<br/>faixas, encargos, IOF e método de amortização<br/>1 entidade · 5 rotas"]
AtributoValor
Identificadorproducts
CategoriaFinanceiro
EscopoTenant (exige organizationId no token em todas as 5 rotas)
Porta (standalone)3004
Path alias@products
Prefixo HTTP/products
Rota completa/products/api/v1/products — o nome do módulo aparece duas vezes, e é assim mesmo
Schema PostgreSQLproducts
StatusProdução
Depende dePostgreSQL, Redis (publicação de eventos), IAM
PermissõesPRODUCTS_CREATE, PRODUCTS_READ, PRODUCTS_UPDATE, PRODUCTS_DELETE

02

O problema

negócio

O cenário. Uma fintech de crédito lança o primeiro produto. Empréstimo pessoal, de mil a cinquenta mil reais, de 3 a 48 parcelas, taxa entre 1,99% e 4,50% ao mês. Esses números entram no código da esteira porque é o caminho mais rápido, e funcionam.

O que trava hoje.

  • Mudar um número exige deploy. Subir o teto de prazo de 48 para 60 parcelas não é uma decisão técnica, é uma decisão comercial. Mas vira ticket, sprint e janela de release. O comercial pede na segunda e recebe no mês seguinte.
  • O parâmetro vive em vários lugares. O simulador do site tem uma faixa, a esteira tem outra, a calculadora de CET tem uma terceira. Quando divergem, o cliente vê uma parcela na simulação e outra no contrato.
  • Ninguém consegue responder qual era a taxa em março. O valor está numa constante que foi sobrescrita. O histórico existe no git log, se alguém souber onde procurar, e não existe para o auditor.
  • As alíquotas de IOF mudam por decreto. Quando muda, é preciso caçar onde o número está escrito. Se estiver em três lugares, dois vão ficar para trás.
  • Amortização diferente de Price e SAC vira caso especial. Balão, carência, parcela crescente e cronograma customizado são operações reais no crédito brasileiro, e cada uma entra no código como um if — que ninguém revisa.

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 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 que deve ser lido como tal. O custo indireto é a divergência: parâmetro em três lugares é a origem mais comum de simulação que não bate com contrato, e essa é a reclamação que chega ao Procon.


03

Proposta de valor

negócio
AntesDepois
Faixa de valor e taxa em constante no códigoRegistro consultável por GET /products/:productId
Mudar o teto de prazo exige deployChamada de API por quem tem PRODUCTS_UPDATE
Simulador, esteira e calculadora com faixas diferentesUma fonte, três consumidores
Balão e carência viram if no código da esteiraamortizationMethod com configuração validada por método
Alíquota de IOF escrita em três lugaresDuas colunas no produto, alimentando o Calculations Engine

Os parâmetros são tipados, não texto livre. Faixa de valor em decimal com duas casas, taxa em decimal com oito casas, parcelas em inteiro, alíquotas de IOF em decimal com dez casas. Isso é o que permite o Calculations Engine consumir sem conversão no meio.

A validação é do domínio, não genérica. Mínimo não pode passar do máximo em valor, taxa nem parcelas. LTV só é aceito em produto do tipo MORTGAGE. Balão exige percentual entre 1% e 99%. Carência de amortização exige o método que vale depois dela. São regras de crédito escritas no serviço, não convenções documentadas em algum lugar.

Oito métodos de amortização, com configuração própria. Price, SAC, balão, bullet, só-juros com carência, degrau crescente, degrau decrescente e customizado. Cada método com a configuração que precisa e nenhuma que não precisa.

Cada mudança vira evento. products.product.created, .updated e .deleted são publicados no fluxo de eventos da plataforma. Quem quiser reagir — auditoria, webhook, cache do simulador — se inscreve.


04

Casos de uso reais

negócio

Caso 1 — O teto de prazo muda numa tarde, não numa sprint Cenário ilustrativo

Contexto

Fintech de crédito pessoal com três produtos ativos e uma esteira própria de originação.

A dor

A concorrência subiu o prazo máximo para 60 parcelas numa quinta-feira. A mudança equivalente na fintech era um número em uma constante do repositório da esteira: precisava de pull request, revisão, aprovação e janela de deploy. Dez dias de atraso comercial para alterar um inteiro.

A solução com o BB

As faixas saem do produto. Quem tem PRODUCTS_UPDATE cria a versão nova do produto com maxInstallments: 60 e desativa a anterior com active: false. A esteira lê GET /products/api/v1/products?filter[active]=true e passa a oferecer o prazo novo na próxima requisição.

flowchart LR
    subgraph Antes["Antes — dez dias"]
      A1["Decisão comercial"] --> A2["Ticket"] --> A3["Pull request"] --> A4["Revisão"] --> A5["Janela de deploy"] --> A6["Prazo novo no ar"]
    end
    subgraph Depois["Depois — uma tarde"]
      D1["Decisão comercial"] --> D2["POST /products com maxInstallments 60"] --> D3["PATCH active false no produto antigo"] --> D4["A esteira lê filter active true<br/>e já oferece o prazo novo"]
    end
O resultado

A mudança comercial vira decisão comercial. O time de engenharia sai do caminho crítico de uma alteração que nunca foi técnica.

Caso 2 — O simulador e o contrato passam a concordar Cenário ilustrativo

Contexto

Operação de crédito com site público que simula parcela, aplicativo que reproduz a simulação e esteira que fecha o contrato.

A dor

Cada um dos três tinha a própria cópia das faixas. Depois de um ajuste de taxa aplicado só na esteira, o site simulava 2,19% e o contrato saía com 2,49%. A reclamação chegou por reclamação de cliente, não por monitoramento.

A solução com o BB

Os três passam a ler o mesmo produto. As alíquotas de IOF (iofAdditionalRate e iofDailyRate), a tarifa de cadastro (registrationTariffRate) e o seguro (insuranceRate) descem do produto para o Calculations Engine, que devolve parcela, IOF e CET a partir dos mesmos números em todos os canais.

flowchart LR
    P["Products<br/>um registro, um conjunto de números"] --> CE["Calculations Engine<br/>parcela · IOF · CET"]
    CE --> S["Site — simula 2,19%"]
    CE --> A["Aplicativo — simula 2,19%"]
    CE --> E["Esteira — contrata 2,19%"]
    P -.->|"taxa, TAC, seguro, IOF adicional e IOF diário"| CE
O resultado

A divergência entre simulação e contrato deixa de ser possível por construção, porque não há mais duas cópias para divergir.

Caso 3 — Um produto com balão sai do if e vira configuração Cenário ilustrativo

Contexto

Financeira de crédito com veículo em garantia que lança uma linha com parcela final ampliada — o balão de 30% do principal.

A dor

O cálculo do balão estava embutido no código da calculadora como caso especial. Nenhum outro sistema sabia que aquele produto tinha balão, então o motor de decisão avaliava a capacidade de pagamento pela parcela mensal e ignorava a parcela final — que é justamente a que quebra o tomador.

A solução com o BB

O produto declara amortizationMethod: "BALLOON" e amortizationConfig: { type: "BALLOON", config: { balloonPercentage: 0.30 } }. O serviço valida que a configuração combina com o método e que o percentual está entre 1% e 99%. Qualquer consumidor que leia o produto enxerga o balão.

flowchart LR
    In["POST /products com amortizationMethod BALLOON"] --> Z["Zod: união discriminada escolhe o schema pelo campo type"]
    Z --> V1{"o type da configuração é igual<br/>ao amortizationMethod?"}
    V1 -->|não| E1["400 VALIDATION"]
    V1 -->|sim| V2{"balloonPercentage entre 0,01 e 0,99?"}
    V2 -->|não| E1
    V2 -->|sim| OK["Produto gravado com o balão declarado no dado"]
    OK --> C1["Calculations Engine enxerga a parcela final"]
    OK --> C2["Decision Platform avalia a capacidade de pagamento<br/>considerando a parcela que quebra o tomador"]
O resultado

A característica do produto vira dado, e para de ser conhecimento tácito de quem escreveu a calculadora.

Caso 4 — A alíquota de IOF muda por decreto e o produto acompanha Referência de mercado

Contexto

As alíquotas de IOF sobre operações de crédito foram alteradas em 2025 pelos Decretos 12.466 e 12.467, com efeito sobre a taxa diária e a alíquota adicional das operações.

A dor

A dor aqui é do mercado inteiro, não de um cliente: instituições que guardavam alíquota de IOF em constante de código precisaram encontrar todas as ocorrências e publicar em regime de urgência. Quem tinha o número em três lugares descobriu o terceiro depois.

flowchart LR
    D["Decreto altera a alíquota de IOF"] --> Q{"Onde a alíquota está guardada?"}
    Q -->|"Em constante de código"| C1["Caçar todas as ocorrências"] --> C2["Pull request em regime de urgência"] --> C3["Deploy de todos os sistemas afetados"] --> C4["Descobrir o terceiro lugar depois"]
    Q -->|"Em coluna do produto"| P1["Atualizar o registro"] --> P2["Calculations Engine calcula com o valor novo<br/>na chamada seguinte"] --> P3["Ressalva: a alteração é DESTRUTIVA<br/>a alíquota anterior não é preservada"]
A solução

Como a Catalisa endereça: iofAdditionalRate e iofDailyRate são colunas do produto, com dez casas decimais de precisão. Mudar a alíquota é atualizar um registro, e o Calculations Engine passa a calcular com o valor novo na chamada seguinte.

O resultado

A mudança regulatória vira operação de dados. Com uma ressalva honesta e importante: neste building block a alteração é destrutiva — não há versão que preserve a alíquota anterior para os contratos já assinados sob ela. Quem precisa disso precisa do Banking Product Portfolio, e essa é a diferença central entre os dois.


05

Mercado e diferenciais

negócio

Panorama: a categoria existe, mas nunca é vendida separada

Panorama. Configuração de produto de crédito é uma categoria estabelecida — só não é vendida separada. Ela chega embutida num core de crédito ou num sistema de originação. O caso mais bem documentado é o da Mambu, cuja especificação pública de loan products confirma o mesmo conjunto de eixos que este building block modela: faixa de valor com mínimo, máximo e padrão; faixa de juros com piso e teto; número de parcelas e unidade do período; carência com tipo PAY_INTEREST_ONLY; tarifas predefinidas; e método de amortização com valores como STANDARD_PAYMENTS e BALLOON_PAYMENTS.

Há, porém, um vazio nítido nessa especificação, e ele é o argumento central desta seção: a Mambu não modela Tabela Price, não modela SAC e não modela IOF. Os métodos de cálculo de juros dela são FLAT, DECLINING_BALANCE, DECLINING_BALANCE_DISCOUNTED e EQUAL_INSTALLMENTS — vocabulário internacional, que não é o vocabulário da regulação brasileira. E a regulação é explícita: a Resolução CMN 4.846/2020, art. 3º, IV, determina que o saldo devedor e as parcelas sejam apurados conforme "o Sistema Francês de Amortização (Tabela Price) mensal, com base de cálculo anual de 360 dias" ou "o Sistema de Amortização Constante (SAC) mensal, com base de cálculo anual de 252, 360 ou 365 dias". A Resolução CMN 4.676/2018 nomeia SAC e SACRE no financiamento imobiliário. Nenhuma das duas define a fórmula — elas tratam os sistemas como termos consagrados —, mas as duas os exigem pelo nome.

A concorrência real do Products, então, é dupla. De um lado, os motores de produto de core banking: maduros, completos, e que chegam com um core inteiro atrás e sem o vocabulário brasileiro. Do outro, a constante no código, que é gratuita e resolve — até o dia em que o parâmetro precisa mudar rápido ou ser lido por três sistemas ao mesmo tempo.

flowchart LR
    subgraph V["O vazio no vocabulário internacional"]
      M["Mambu — métodos de cálculo de juros<br/>FLAT · DECLINING_BALANCE<br/>DECLINING_BALANCE_DISCOUNTED · EQUAL_INSTALLMENTS"]
      R["Regulação brasileira<br/>Resolução CMN 4.846/2020, art. 3º, IV<br/>Tabela Price mensal, base anual de 360 dias<br/>SAC mensal, base anual de 252, 360 ou 365 dias"]
      M -.->|"não mapeia um para um"| R
    end
    V --> T["Quem adota motor internacional escreve<br/>a camada de tradução — e depois a mantém"]
    R --> P["Catalisa Products<br/>PRICE e SAC como valores do enum,<br/>IOF adicional e IOF diário como colunas"]

O comparativo, critério a critério

CritérioCatalisa ProductsConstante no códigoMambu Loan ProductsVault Core (Thought Machine)
Base de cobrançaPrecificação em definiçãoEngenharia e janela de releaseAssinatura, sem preço públicoLicença, sem preço público
Mudar parâmetro sem deploySimNãoSimSim
Parâmetro consultável por APISimNãoSimSim
Faixa de valor, juros e parcelasSimVocê implementaSimVocê escreve em Python
Tabela Price e SAC nominaisSimVocê implementaNãoVocê implementa
IOF adicional e diárioSim, colunas dedicadasVocê implementaNãoVocê implementa
Tarifa de cadastro e seguroColunas do produtoVocê implementaTarifas predefinidasVocê implementa
Balão e carência de amortizaçãoSim, com validação por métodoVocê implementaSimVocê implementa
Versão congelada por contratoNão — é o banking-product-portfolioNãoSimSim
Aprovação formal e segregação de funçõesNãoNãoSimSim
Variante por canal ou convênioNãoNãoSimSim
Cálculo financeiro embutidoNão — é o calculations-engineVocê implementaSimSim
Adoção sem trocar o coreSimSimNãoNão
Esforço de implantaçãoCinco rotasZeroProjeto de bancoProjeto de banco

Linhas da Mambu conferidas em 2026-08-16 contra a especificação OpenAPI pública de loan products. Mambu e Thought Machine não publicam preço em página aberta, e este documento não estima valor para nenhum dos dois. A linha do Vault Core descreve o posicionamento público do fornecedor — não conferimos a documentação técnica dele nesta redação, então trate-a como referência de categoria, não como verificação. Consultamos também nCino, Temenos e Finastra e não conseguimos abrir página pública com detalhe suficiente sobre configuração de produto; por isso os três ficaram fora do comparativo.

Nossos diferenciais

Nossos diferenciais

  1. O vocabulário é o da regulação brasileira. Tabela Price e SAC são valores do enum, não interpretação de DECLINING_BALANCE. IOF adicional e IOF diário são colunas com dez e doze dígitos de precisão. O líder global de core lending não tem nenhum dos três — e não é descuido: são conceitos de um mercado só. Quem adota um motor internacional escreve essa camada de tradução, e depois a mantém.
  2. O produto sai do código sem trazer um core junto. Adotar o Products é apontar a esteira para cinco rotas. Adotar o motor de produto do Mambu ou do Vault Core é um projeto de substituição de core banking. Para uma fintech com dois ou três produtos, a diferença de esforço é de ordens de grandeza.
  3. A validação de amortização é do domínio. Balão exige percentual entre 1% e 99%. Só-juros exige o método que vale depois da carência. Degrau exige percentual até 50% e intervalo. LTV só existe em produto do tipo MORTGAGE. São regras de crédito no serviço, não documentação que alguém precisa lembrar de seguir.

Quando escolher o concorrente

Se o seu requisito éA escolha certa éPor quê
Operação fora do BrasilUm motor internacional (Mambu, Vault Core)O diferencial de vocabulário desaparece, e eles são mais completos em tudo o mais
O contrato assinado continuar valendo sob a condição da data em que foi assinadoBanking Product PortfolioA alteração no Products é destrutiva — ele não faz isso e nunca vai fazer
Família, variante por canal ou convênio, e aprovação formal com segregação de funçõesBanking Product PortfolioÉ o bloco desenhado para isso
Trocar o core de qualquer jeitoO motor de produto do Mambu ou do Vault CoreVem junto e é mais completo que este BB em todas as dimensões — escolha com os olhos abertos
Um produto só, que muda uma vez por anoA constante no códigoNão troque simplicidade por infraestrutura sem motivo
Poucos produtos de crédito, parâmetros que mudam com frequência comercial, e mais de um sistema lendo o mesmo númeroCatalisa ProductsÉ exatamente o problema que ele resolve

Quando escolher o concorrente. Se a sua operação não é brasileira, o diferencial de vocabulário desaparece e um motor internacional é mais completo em tudo o mais. Se você precisa que o contrato assinado continue valendo sob a condição da data em que foi assinado, o Products não faz isso e nunca vai fazer — a alteração aqui é destrutiva. Use o Banking Product Portfolio, que existe exatamente para isso. Se você precisa de família, variante por canal ou convênio, e aprovação formal com segregação de funções, é o mesmo caminho. Se a sua instituição vai trocar o core de qualquer jeito, o motor de produto do Mambu ou do Vault Core vem junto e é mais completo que este building block em todas as dimensões — vale escolher um deles com os olhos abertos. E se você tem um produto só, que muda uma vez por ano, a constante no código continua sendo a resposta certa; não troque simplicidade por infraestrutura sem motivo. O Products ganha quando o problema é poucos produtos de crédito, parâmetros que mudam com frequência comercial, e mais de um sistema precisando ler o mesmo número.


06

Modelo de cobrança e ROI

negócio

Precificação em definição

Precificação em definição. O Products não tem preço fechado. Ele é um bloco de fundação, contratado junto com os blocos que consomem os parâmetros — Calculations Engine, Pricing Engine, Decision Platform. Não há valor a divulgar, e este documento não estima nenhum.

O que dispara custo

O que dispara custo.

DriverPor quê
Produtos ativos no catálogoÉ a unidade que o cliente entende e a que cresce com a operação
Chamadas de APILeitura de produto acontece a cada simulação, não a cada contrato

Comparação de custo

Comparação de custo. Não há comparação de preço defensável a apresentar. Mambu e Thought Machine não publicam preço, e mesmo que publicassem, o objeto de compra deles é um core banking inteiro — comparar o preço de um core com o de um configurador de produto produziria um número sem significado. A alternativa mais comum, a constante no código, tem custo de licença zero.

ROI

ROI. A conta tem duas linhas, e nenhuma é licença.

Linha da contaO que se ganhaPor que não colocamos número
Tempo comercialO intervalo até a próxima janela de release deixa de existir para alteração de parâmetroDepende do seu ciclo de release e da sua margem — a conta é sua, e é fácil de fazer
Divergência entre canaisSome a simulação que não bate com o contratoO custo é atendimento, reclamação e risco de conduta; não temos número defensável

A primeira é o tempo comercial. Cada alteração de parâmetro que exige deploy custa, na prática, o intervalo até a próxima janela de release — tipicamente de dias a semanas. Numa operação que ajusta taxa ou prazo em resposta à concorrência, esse intervalo é receita não capturada. O número exato depende do seu ciclo de release e da sua margem; a conta é sua, e é fácil de fazer.

A segunda é a divergência entre canais. Parâmetro em três lugares gera simulação que não bate com contrato. O custo disso não é o estorno — é o atendimento, a reclamação e, no limite, o risco de conduta. Não colocamos número nessa linha porque não temos um defensável.


07

Arquitetura

As camadas e o caminho da requisição

flowchart TD
    HTTP["HTTP — Bearer JWT emitido pelo IAM"]

    subgraph App["Hono app · basePath /products · applyCommonMiddleware"]
      direction TB
      Mid["bodyLimit 1 MB · CORS · security headers · rate limit global"]
      R1["/api/v1/products — productsRouter, 5 rotas"]
      R2["/health — status estático do serviço"]
    end

    subgraph Cad["Cadeia de middlewares, nesta ordem"]
      direction TB
      M1["authMiddleware"] --> M2["requirePermission PRODUCTS_*"] --> M3["requireOrganization — 403 sem organizationId"] --> M4["Zod parse — união discriminada da amortização"]
    end

    subgraph Svc["services/ProductService"]
      direction TB
      V1["min ≤ max em valor, taxa e parcelas"]
      V2["validateAmortizationConfig — a config casa com o método?"]
      V3["validateLtvFields — LTV só em MORTGAGE"]
      V4["nome único DENTRO da organização"]
      V5["grava → publica evento → devolve"]
    end

    Repo["repositories/ProductRepository<br/>Prisma → PostgreSQL, schema products<br/>organizationId em toda leitura<br/>deletedAt = exclusão lógica"]
    Ev["EventPublisher<br/>Redis Stream iam-events<br/>products.product.created, updated e deleted"]
    Cons["audit-trail · webhooks-engine"]

    HTTP --> App --> Cad --> Svc
    Svc --> Repo
    Svc --> Ev
    Ev --> Cons

Quem consome os parâmetros

O Products não chama ninguém. As setas abaixo são chamadas feitas pela sua aplicação, a partir dos parâmetros que ela leu daqui.

flowchart LR
    P["Products"] -->|"taxa, TAC, seguro, IOF e método"| CE["Calculations Engine<br/>parcela, IOF, CET"]
    P -->|"faixa min e max de taxa"| PE["Pricing Engine<br/>taxa dentro da faixa"]
    P -->|"faixa de valor e de prazo"| DP["Decision Platform<br/>elegibilidade por valor e prazo"]

Decisões não óbvias

Decisões não óbvias.

  • O prefixo da rota repete o nome do módulo. A rota completa é /products/api/v1/products: o primeiro products é o basePath do app, o segundo é o recurso. Parece erro e não é. Vale conferir na sua integração, porque /products/api/v1 sozinho devolve 404.
  • A taxa é guardada como decimal, não como percentual. minInterestRate: 0.0199 significa 1,99% ao mês. O schema aceita de 0 a 1, então uma taxa acima de 100% ao mês é reprovada. Enviar 1.99 achando que são 1,99% cria um produto com 199% ao mês e passa na validação — é a armadilha de integração mais provável deste BB.
  • O nome é único dentro da organização, não na plataforma. nameExists(name, organizationId) inclui a organização na checagem. Duas empresas clientes podem ter, cada uma, um produto chamado "Crédito Pessoal". É o comportamento correto e vale contrastar com o Customers, onde o CPF é único globalmente.
  • A configuração de amortização é uma união discriminada por type. O Zod escolhe o schema pelo campo type, e o serviço confere depois se o type bate com o amortizationMethod. A dupla checagem é redundante de propósito: a primeira dá erro de campo legível, a segunda impede combinação inconsistente.
  • PRICE e SAC não aceitam configuração e não precisam. validateAmortizationConfig retorna cedo para os dois. Mandar amortizationConfig junto de amortizationMethod: "PRICE" é reprovado, porque o type não pode casar.
  • O PATCH altera apenas description e active. Faixa de valor, taxa, prazo, IOF e método de amortização não são alteráveis depois da criação. É a decisão de arquitetura mais importante deste BB e a mais fácil de interpretar errado: ela protege contra alteração acidental de condição comercial, mas não é versionamento. Alterar condição significa criar um produto novo e desativar o antigo, e os contratos que apontam para o produto antigo continuam apontando para ele. Ver §12 e §15.
  • A publicação do evento faz parte da cadeia de escrita. Se o Redis estiver fora, a criação devolve 500. Escolha deliberada: parâmetro alterado sem evento produz auditoria com buraco.
  • A exclusão é lógica. deletedAt preenchido, linha preservada, some de todas as consultas. Contrato antigo que referencie o productId continua conseguindo... nada — o GET devolve 404. Trate a exclusão como definitiva do ponto de vista de integração.

Monolito vs. standalone. O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/products. O main.ts sobe o mesmo app na porta 3004 quando DEPLOYMENT_MODE=standalone — o modo usado em staging e produção. Não há diferença de comportamento: o BB não chama nenhum outro building block. Em standalone, o applyCommonMiddleware é a única fonte de limite de corpo, CORS, cabeçalhos de segurança e limite de taxa.


08

Conceitos e modelo de dados

Glossário

TermoSignifica
ProductUm produto de crédito de uma organização, com faixas e encargos. É a única entidade do BB.
FaixaTodo parâmetro comercial é um par mínimo/máximo: valor, taxa e parcelas. A esteira escolhe dentro da faixa; o produto define o limite.
PRICESistema Francês de Amortização, a Tabela Price: parcela constante. Nomeado assim na Resolução CMN 4.846/2020, art. 3º, IV, com base de cálculo anual de 360 dias. A norma nomeia o sistema; não define a fórmula.
SACSistema de Amortização Constante: amortização fixa, parcela decrescente. Também nomeado na Resolução CMN 4.846/2020, art. 3º, IV, com base anual de 252, 360 ou 365 dias, e na Resolução CMN 4.676/2018 para financiamento imobiliário.
Taxa em decimal0.0199 é 1,99% ao mês. Aceita de 0 a 1. Nunca em pontos percentuais.
IOF adicionalAlíquota única incidente sobre o valor da operação. Coluna iofAdditionalRate.
IOF diárioAlíquota por dia de prazo. Coluna iofDailyRate, com dez casas decimais porque a alíquota é da ordem de 0,000082.
Tarifa de cadastro (TAC)registrationTariffRate, em decimal sobre o valor da operação.
Carência (gracePeriodMonths)Meses antes de a amortização começar. É um número de meses no produto; o efeito no cronograma é do Calculations Engine.
LTVLoan-to-Value, a razão entre o empréstimo e o valor do bem. Aceito somente em produto do tipo MORTGAGE.
metadataJSONB livre para atributo próprio da organização. Não é validado, não é indexado, não é filtrável.
Exclusão lógicadeletedAt preenchido. Sai de todas as consultas e permanece na tabela. Não há rota de restauração.

Modelo de dados — schema products no PostgreSQL. 1 modelo.

Modelo PrismaTabelaPropósitoCampos-chave
Productproducts.productsProduto de crédito com faixas e encargosorganizationId, name (único por organização, validado no serviço), productType, amortizationMethod, active, deletedAt

Campos por grupo:

GrupoColunasTipoObrigatório
Identificaçãoname, productType, description, activetexto / enum / texto / booleanoSim (active tem padrão true)
Faixa de valorminAmount, maxAmountDecimal(15,2)Sim
Faixa de taxaminInterestRate, maxInterestRateDecimal(10,8)Sim
Faixa de prazominInstallments, maxInstallmentsinteiro, 1 a 360Sim
EncargosregistrationTariffRate, insuranceRateDecimal(10,8)Não
IOFiofAdditionalRate Decimal(10,8), iofDailyRate Decimal(12,10)decimalNão
AmortizaçãoamortizationMethod (padrão PRICE), amortizationConfig JSONB, gracePeriodMonths—Não
GarantiaminLtv, maxLtvDecimal(5,4)Não, e só em MORTGAGE
ExtensãometadataJSONBNão
ControlecreatedAt, updatedAt, deletedAttimestampAutomático

Índices: organizationId, productType, active, deletedAt.

erDiagram
    ORGANIZATION ||--o{ PRODUCT : "cadastra"

    PRODUCT {
        uuid id PK
        uuid organization_id FK "isolamento do tenant — índice"
        string name "1 a 100, único DENTRO da organização"
        string product_type "8 valores — índice"
        text description "1 a 5.000"
        boolean active "padrão true — índice"
        decimal min_amount "Decimal 15,2 — piso da faixa de valor"
        decimal max_amount "Decimal 15,2 — teto da faixa de valor"
        decimal min_interest_rate "Decimal 10,8 — 0.0199 é 1,99% ao mês"
        decimal max_interest_rate "Decimal 10,8 — de 0 a 1, nunca em pontos percentuais"
        int min_installments "de 1 a 360"
        int max_installments "de 1 a 360"
        decimal registration_tariff_rate "Decimal 10,8 — TAC, opcional"
        decimal insurance_rate "Decimal 10,8 — seguro, opcional"
        decimal iof_additional_rate "Decimal 10,8 — alíquota única sobre a operação"
        decimal iof_daily_rate "Decimal 12,10 — alíquota por dia de prazo"
        string amortization_method "padrão PRICE — 8 métodos"
        jsonb amortization_config "exigida por método — ver a tabela abaixo"
        int grace_period_months "meses antes de a amortização começar"
        decimal min_ltv "Decimal 5,4 — SÓ em MORTGAGE"
        decimal max_ltv "Decimal 5,4 — SÓ em MORTGAGE"
        jsonb metadata "livre, sem validação, sem índice, não filtrável"
        timestamp created_at
        timestamp updated_at
        timestamp deleted_at "exclusão lógica — índice"
    }

Atenção. Não há chave estrangeira entre Product e nenhuma entidade de contrato, proposta ou pessoa. O vínculo com o Customers e com a operação é feito pela sua aplicação, guardando o productId. É o que torna a exclusão lógica perigosa: o Products não sabe quem o referencia.

Enumerações

EnumValores
ProductTypePERSONAL_LOAN · PAYROLL_LOAN · VEHICLE_LOAN · HOME_EQUITY · CREDIT_CARD · WORKING_CAPITAL · INVOICE_FINANCING · MORTGAGE
AmortizationMethodPRICE · SAC · BALLOON · BULLET · INTEREST_ONLY · STEP_UP · STEP_DOWN · CUSTOM

Uma versão anterior desta documentação listava ProductType com os valores LOAN e CREDIT_LINE, e AmortizationMethod só com SAC e PRICE. Nenhum dos dois confere. A fonte é src/products/types/index.ts.

Configuração exigida por método de amortização

MétodoamortizationConfigCamposValidação
PRICENão aceita—Parcela fixa. Configuração é ignorada pela validação e reprovada pelo casamento de type
SACNão aceita—Amortização constante
BALLOONObrigatóriaballoonPercentageDe 0,01 a 0,99
BULLETOpcional—Principal e juros no vencimento
INTEREST_ONLYObrigatóriainterestOnlyPeriod, postGraceMethodPeríodo ≥ 1; método posterior é PRICE ou SAC
STEP_UPObrigatóriastepPercentage, stepIntervalPercentual de 0,01 a 0,50; intervalo ≥ 1
STEP_DOWNObrigatóriastepPercentage, stepIntervalMesmas regras do STEP_UP
CUSTOMObrigatóriapaymentSchedule ou formulaAo menos um dos dois

PRICE e SAC são os únicos que dispensam configuração. O type dentro de amortizationConfig precisa ser igual ao amortizationMethod, senão a criação devolve 400.

O mesmo dito como decisão, na ordem em que a validação acontece:

flowchart TD
    M{"amortizationMethod"}
    M -->|"PRICE ou SAC"| N["Não aceita amortizationConfig<br/>a validação retorna cedo"]
    M -->|BULLET| O["Configuração opcional<br/>principal e juros no vencimento"]
    M -->|BALLOON| B["Obrigatória: balloonPercentage<br/>de 0,01 a 0,99"]
    M -->|INTEREST_ONLY| I["Obrigatória: interestOnlyPeriod ≥ 1<br/>e postGraceMethod PRICE ou SAC"]
    M -->|"STEP_UP ou STEP_DOWN"| S["Obrigatória: stepPercentage de 0,01 a 0,50<br/>e stepInterval ≥ 1"]
    M -->|CUSTOM| C["Obrigatória: paymentSchedule OU formula<br/>ao menos um dos dois"]
    N --> T{"o type dentro de amortizationConfig<br/>é IGUAL ao amortizationMethod?"}
    O --> T
    B --> T
    I --> T
    S --> T
    C --> T
    T -->|não| E["400 VALIDATION"]
    T -->|sim| OK["Produto criado"]

Ciclo de vida de um produto

Ciclo de vida de um produto

stateDiagram-v2
    direction LR
    [*] --> ativo: POST /products

    ativo: ativo — active = true
    inativo: inativo — active = false
    excluido: excluído logicamente — deletedAt preenchido

    ativo --> inativo: PATCH active false
    inativo --> ativo: PATCH active true
    ativo --> excluido: DELETE /products/:id
    inativo --> excluido: DELETE /products/:id
    excluido --> [*]

    note right of excluido
        Sai de TODAS as consultas.
        Não há rota de restauração.
        GET passa a devolver 404 — quem
        guardou o productId perde a leitura.
    end note

Três ausências que o diagrama torna visíveis, e que são a razão de existir o Banking Product Portfolio:

Não existeConsequência
Estado rascunho, publicado, aprovado ou depreciadoQuem tem PRODUCTS_CREATE cria produto sozinho, e ele já nasce ofertável
VersãoMudar condição = criar produto novo + desativar o antigo
Congelamento por contratoQuem precisa de versão publicada e aprovação usa o banking-product-portfolio

09

Referência da API

Prefixo: /products. Em monolito, a base é http://localhost:3000. Em staging, https://products.bb.stg.catalisa.app.

Todas as 5 rotas exigem, sem exceção: authMiddleware (Bearer JWT do IAM), requirePermission(...) e o middleware local requireOrganization, que devolve 403 quando o token não carrega organizationId.

Produtos — /products/api/v1/products

MétodoRotaDescriçãoPermissão
POST/products/api/v1/productsCria produto. Responde 201PRODUCTS_CREATE
GET/products/api/v1/productsLista produtos, paginado e filtrávelPRODUCTS_READ
GET/products/api/v1/products/:productIdBusca um produtoPRODUCTS_READ
PATCH/products/api/v1/products/:productIdAtualiza description e/ou activePRODUCTS_UPDATE
DELETE/products/api/v1/products/:productIdExclusão lógica. Responde 204 sem corpoPRODUCTS_DELETE

Saúde do serviço

MétodoRotaDescrição
GET/products/healthStatus estático do serviço. Pública, não contabilizada nas 5 rotas

A sonda não consulta o banco: responde {"status":"ok","service":"products","version":...} enquanto o processo estiver de pé.


POST /products/api/v1/products

Atenção ao envelope. O corpo é aninhado em data.attributes.

Request

json
{
  "data": {
    "type": "products",
    "attributes": {
      "name": "Crédito Pessoal Digital",
      "productType": "PERSONAL_LOAN",
      "description": "Empréstimo pessoal sem garantia para clientes com conta ativa.",
      "active": true,
      "minAmount": { "amount": 1000.00, "currency": "BRL" },
      "maxAmount": { "amount": 50000.00, "currency": "BRL" },
      "minInterestRate": 0.0199,
      "maxInterestRate": 0.0450,
      "minInstallments": 3,
      "maxInstallments": 48,
      "registrationTariffRate": 0.02,
      "insuranceRate": 0.005,
      "iofAdditionalRate": 0.0038,
      "iofDailyRate": 0.000082,
      "amortizationMethod": "PRICE",
      "gracePeriodMonths": 0,
      "metadata": { "canal": "app", "convenio": null }
    }
  }
}
{
  "data": {
    "type": "products",
    "attributes": {
      "name": "Crédito Pessoal Digital",
      "productType": "PERSONAL_LOAN",
      "description": "Empréstimo pessoal sem garantia para clientes com conta ativa.",
      "active": true,
      "minAmount": { "amount": 1000.00, "currency": "BRL" },
      "maxAmount": { "amount": 50000.00, "currency": "BRL" },
      "minInterestRate": 0.0199,
      "maxInterestRate": 0.0450,
      "minInstallments": 3,
      "maxInstallments": 48,
      "registrationTariffRate": 0.02,
      "insuranceRate": 0.005,
      "iofAdditionalRate": 0.0038,
      "iofDailyRate": 0.000082,
      "amortizationMethod": "PRICE",
      "gracePeriodMonths": 0,
      "metadata": { "canal": "app", "convenio": null }
    }
  }
}
CampoTipoObrigatórioRegra
namestringSim1 a 100 caracteres. Único dentro da sua organização
productTypeenumSimVer §8
descriptionstringSim1 a 5.000 caracteres
activebooleanNãoPadrão true
minAmount.amountnumberSimPositivo. min ≤ max
maxAmount.amountnumberSimPositivo
minInterestRatenumberSimDe 0 a 1. Decimal, não percentual
maxInterestRatenumberSimDe 0 a 1. min ≤ max
minInstallmentsinteiroSimDe 1 a 360. min ≤ max
maxInstallmentsinteiroSimDe 1 a 360
registrationTariffRatenumberNãoDe 0 a 1
insuranceRatenumberNãoDe 0 a 1
iofAdditionalRatenumberNãoDe 0 a 1
iofDailyRatenumberNãoDe 0 a 1
amortizationMethodenumNãoPadrão PRICE
amortizationConfigobjetoDepende do métodoVer a tabela da §8
gracePeriodMonthsinteiroNão≥ 0
minLtv / maxLtvnumberNãoDe 0 a 1. Só em MORTGAGE; minLtv ≤ maxLtv
metadataobjetoNãoJSON livre, sem validação

Resposta 201

json
{
  "data": {
    "type": "products",
    "id": "8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88",
    "links": { "self": "/api/v1/products/8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88" },
    "attributes": {
      "name": "Crédito Pessoal Digital",
      "productType": "PERSONAL_LOAN",
      "active": true,
      "minAmount": { "amount": 1000, "currency": "BRL" },
      "maxAmount": { "amount": 50000, "currency": "BRL" },
      "minInterestRate": 0.0199,
      "maxInterestRate": 0.045,
      "minInstallments": 3,
      "maxInstallments": 48,
      "amortizationMethod": "PRICE",
      "createdAt": "2026-08-16T13:40:02.114Z",
      "updatedAt": "2026-08-16T13:40:02.114Z"
    }
  },
  "links": { "self": "/api/v1/products/8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88" }
}
{
  "data": {
    "type": "products",
    "id": "8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88",
    "links": { "self": "/api/v1/products/8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88" },
    "attributes": {
      "name": "Crédito Pessoal Digital",
      "productType": "PERSONAL_LOAN",
      "active": true,
      "minAmount": { "amount": 1000, "currency": "BRL" },
      "maxAmount": { "amount": 50000, "currency": "BRL" },
      "minInterestRate": 0.0199,
      "maxInterestRate": 0.045,
      "minInstallments": 3,
      "maxInstallments": 48,
      "amortizationMethod": "PRICE",
      "createdAt": "2026-08-16T13:40:02.114Z",
      "updatedAt": "2026-08-16T13:40:02.114Z"
    }
  },
  "links": { "self": "/api/v1/products/8d2b1f47-3c5a-4e19-b0d6-7a1e9c4f2b88" }
}

Os campos monetários voltam como número, e a moeda é sempre BRL — a coluna guarda apenas o valor, e a moeda é fixada na serialização. O links.self é relativo e não inclui o prefixo /products.

Erros

StatusCódigoQuando
400—Corpo reprovado no Zod. details[] traz field, message e code
400VALIDATIONmin maior que max em valor, taxa ou parcelas
400VALIDATIONamortizationConfig.type diferente do amortizationMethod
400VALIDATIONMétodo que exige configuração enviado sem ela
400VALIDATIONminLtv/maxLtv em produto que não é MORTGAGE
400VALIDATIONgracePeriodMonths negativo
401—Token ausente, inválido ou expirado
403—Falta PRODUCTS_CREATE, ou o token não carrega organizationId
409CONFLICTJá existe produto com esse nome na sua organização

GET /products/api/v1/products

Parâmetros de consulta

ParâmetroTipoPadrãoDescrição
page[number]inteiro1Página, mínimo 1
page[size]inteiro20Itens por página, máximo 100
filter[active]true | false—Qualquer valor diferente de true é tratado como false
filter[productType]texto—Igualdade exata contra o valor do enum

Ordenação fixa: createdAt decrescente.

Resposta 200 — mesma forma do Customers: data[], meta com totalItems, totalPages, currentPage, itemsPerPage, hasNextPage, hasPreviousPage, e links de paginação.


PATCH /products/api/v1/products/:productId

Aceita apenas description e active. Não aceita data.attributes como envelope — diferente do Customers, aqui o corpo é plano.

json
{ "active": false }
{ "active": false }
CampoTipoRegra
descriptionstring1 a 5.000 caracteres
activeboolean—

Nome, faixas, taxas, IOF, método de amortização e LTV não são alteráveis. Para mudar condição comercial, crie produto novo e desative o antigo.

Responde 200 com o produto atualizado, ou 404 NOT_FOUND quando o productId não existe na sua organização. productId que não seja UUID devolve 400.


DELETE /products/api/v1/products/:productId

Exclusão lógica. Responde 204 sem corpo. Chamar de novo devolve 404.


10

Início rápido

Do zero ao primeiro produto consultável. Credenciais em AMBIENTES.md.

Os comandos abaixo não foram executados na redação deste documento. As respostas são as previstas pelo código.

flowchart LR
    P1["1. Autenticar no IAM"] --> P2["2. Criar o primeiro produto"]
    P2 --> Ver{"minInterestRate voltou 0.0199?"}
    Ver -->|"voltou 1.99"| Erro["Você criou um produto com 199% ao mês.<br/>Apague e refaça — a validação não pega isso"]
    Ver -->|sim| P3["3. Listar só os ativos"]
    P3 --> P4["4. Ler um produto"]
    P4 --> P5["5. Confirmar que o isolamento é real"]

1. Autenticar no IAM

bash
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/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)
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/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 o primeiro produto

bash
API=https://products.bb.stg.catalisa.app/products/api/v1

PRODUCT=$(curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "products", "attributes": {
      "name": "Crédito Pessoal Digital",
      "productType": "PERSONAL_LOAN",
      "description": "Empréstimo pessoal sem garantia.",
      "minAmount": { "amount": 1000, "currency": "BRL" },
      "maxAmount": { "amount": 50000, "currency": "BRL" },
      "minInterestRate": 0.0199,
      "maxInterestRate": 0.0450,
      "minInstallments": 3,
      "maxInstallments": 48,
      "amortizationMethod": "PRICE"
    }}
  }')

PRODUCT_ID=$(echo "$PRODUCT" | jq -r '.data.id')
echo "$PRODUCT" | jq '.data.attributes | {minInterestRate, maxInterestRate}'
API=https://products.bb.stg.catalisa.app/products/api/v1

PRODUCT=$(curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "products", "attributes": {
      "name": "Crédito Pessoal Digital",
      "productType": "PERSONAL_LOAN",
      "description": "Empréstimo pessoal sem garantia.",
      "minAmount": { "amount": 1000, "currency": "BRL" },
      "maxAmount": { "amount": 50000, "currency": "BRL" },
      "minInterestRate": 0.0199,
      "maxInterestRate": 0.0450,
      "minInstallments": 3,
      "maxInstallments": 48,
      "amortizationMethod": "PRICE"
    }}
  }')

PRODUCT_ID=$(echo "$PRODUCT" | jq -r '.data.id')
echo "$PRODUCT" | jq '.data.attributes | {minInterestRate, maxInterestRate}'
json
{ "minInterestRate": 0.0199, "maxInterestRate": 0.045 }
{ "minInterestRate": 0.0199, "maxInterestRate": 0.045 }

Confira este retorno. 0.0199 é 1,99% ao mês. Se aparecer 1.99, você criou um produto com 199% ao mês.

3. Listar só os ativos

bash
curl -s -G "$API/products" --data-urlencode 'filter[active]=true' \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {name, productType, maxInstallments}'
curl -s -G "$API/products" --data-urlencode 'filter[active]=true' \
  -H "Authorization: Bearer $TOKEN" \
  | jq '.data[] | {name, productType, maxInstallments}'

Resposta prevista — é esta a consulta que a sua esteira faz para montar a oferta:

json
{ "name": "Crédito Pessoal Digital", "productType": "PERSONAL_LOAN", "maxInstallments": 48 }
{ "name": "Crédito Pessoal Digital", "productType": "PERSONAL_LOAN", "maxInstallments": 48 }

4. Ler um produto

bash
curl -s "$API/products/$PRODUCT_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes'
curl -s "$API/products/$PRODUCT_ID" -H "Authorization: Bearer $TOKEN" \
  | jq '.data.attributes'

Resposta prevista: o bloco de atributos completo, com os campos monetários como número e a moeda fixada em BRL na serialização. Os campos que você não informou na criação — registrationTariffRate, insuranceRate, IOF — voltam ausentes, não como zero.

5. Confirmar que o isolamento é real

bash
curl -s -o /dev/null -w "%{http_code}\n" "$API/products" \
  -H "Authorization: Bearer token-invalido"
curl -s -o /dev/null -w "%{http_code}\n" "$API/products" \
  -H "Authorization: Bearer token-invalido"

Retorna 401.


11

Receitas

Criar um produto com balão

Objetivo. Produto com parcela final ampliada de 30% do principal.

bash
curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "products", "attributes": {
      "name": "Veículo com Parcela Final",
      "productType": "VEHICLE_LOAN",
      "description": "Financiamento de veículo com balão de 30%.",
      "minAmount": { "amount": 15000, "currency": "BRL" },
      "maxAmount": { "amount": 200000, "currency": "BRL" },
      "minInterestRate": 0.0129, "maxInterestRate": 0.0289,
      "minInstallments": 12, "maxInstallments": 60,
      "amortizationMethod": "BALLOON",
      "amortizationConfig": {
        "type": "BALLOON",
        "config": { "balloonPercentage": 0.30 }
      }
    }}
  }' | jq '.data.attributes.amortizationConfig'
curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "products", "attributes": {
      "name": "Veículo com Parcela Final",
      "productType": "VEHICLE_LOAN",
      "description": "Financiamento de veículo com balão de 30%.",
      "minAmount": { "amount": 15000, "currency": "BRL" },
      "maxAmount": { "amount": 200000, "currency": "BRL" },
      "minInterestRate": 0.0129, "maxInterestRate": 0.0289,
      "minInstallments": 12, "maxInstallments": 60,
      "amortizationMethod": "BALLOON",
      "amortizationConfig": {
        "type": "BALLOON",
        "config": { "balloonPercentage": 0.30 }
      }
    }}
  }' | jq '.data.attributes.amortizationConfig'

Resposta prevista — a configuração volta como foi gravada, e é ela que qualquer consumidor vai ler para saber que o produto tem balão:

json
{ "type": "BALLOON", "config": { "balloonPercentage": 0.3 } }
{ "type": "BALLOON", "config": { "balloonPercentage": 0.3 } }

Armadilhas.

  • O type dentro de amortizationConfig precisa ser idêntico ao amortizationMethod. BALLOON com type: "BULLET" devolve 400.
  • balloonPercentage é decimal de 0,01 a 0,99. 30 é reprovado; 0.30 é o certo.
  • Omitir amortizationConfig num método que a exige devolve 400 com a mensagem do método.

Criar um produto com carência de amortização

Objetivo. Seis meses pagando só juros, depois Price.

bash
curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "products", "attributes": {
      "name": "Capital de Giro com Carência",
      "productType": "WORKING_CAPITAL",
      "description": "Seis meses de carência de amortização.",
      "minAmount": { "amount": 20000, "currency": "BRL" },
      "maxAmount": { "amount": 500000, "currency": "BRL" },
      "minInterestRate": 0.0159, "maxInterestRate": 0.0349,
      "minInstallments": 12, "maxInstallments": 36,
      "amortizationMethod": "INTEREST_ONLY",
      "amortizationConfig": {
        "type": "INTEREST_ONLY",
        "config": { "interestOnlyPeriod": 6, "postGraceMethod": "PRICE" }
      },
      "gracePeriodMonths": 6
    }}
  }' | jq '.data.id'
curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "data": { "type": "products", "attributes": {
      "name": "Capital de Giro com Carência",
      "productType": "WORKING_CAPITAL",
      "description": "Seis meses de carência de amortização.",
      "minAmount": { "amount": 20000, "currency": "BRL" },
      "maxAmount": { "amount": 500000, "currency": "BRL" },
      "minInterestRate": 0.0159, "maxInterestRate": 0.0349,
      "minInstallments": 12, "maxInstallments": 36,
      "amortizationMethod": "INTEREST_ONLY",
      "amortizationConfig": {
        "type": "INTEREST_ONLY",
        "config": { "interestOnlyPeriod": 6, "postGraceMethod": "PRICE" }
      },
      "gracePeriodMonths": 6
    }}
  }' | jq '.data.id'

Resposta prevista: o identificador do produto criado.

texto
c7f0a3e8-1b44-4d02-9a51-6e83f0b1c227
c7f0a3e8-1b44-4d02-9a51-6e83f0b1c227
flowchart LR
    G["gracePeriodMonths = 6<br/>declarado no produto"] --> CE["Calculations Engine<br/>monta o cronograma"]
    I["interestOnlyPeriod = 6<br/>dentro de amortizationConfig"] --> CE
    G -.->|"NÃO são validados um contra o outro"| I
    CE --> Cron["6 meses pagando só juros,<br/>depois Price"]

Armadilhas.

  • gracePeriodMonths e interestOnlyPeriod são campos diferentes e não são validados um contra o outro. Manter os dois coerentes é responsabilidade sua.
  • postGraceMethod aceita só PRICE ou SAC.
  • O produto declara a carência; quem monta o cronograma é o Calculations Engine.

Alterar a condição comercial de um produto

Objetivo. Subir o prazo máximo de 48 para 60 parcelas.

Não existe caminho de alteração. PATCH só muda description e active. O procedimento é:

flowchart LR
    A["1. Criar o produto novo<br/>com nome diferente — o nome é único por organização"] --> B["2. Desativar o antigo com PATCH active false"]
    B --> C["O antigo continua consultável por GET"]
    B -.->|"NÃO use DELETE"| D["Excluído devolve 404 e quebra<br/>quem guardou o productId"]
    C --> E["Contratos já fechados continuam apontando<br/>para o produto antigo — a condição NÃO é congelada"]

1. Criar o produto novo com o nome ajustado — o nome é único por organização.

bash
NEW=$(curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"data":{"type":"products","attributes":{
        "name":"Crédito Pessoal Digital 60x",
        "productType":"PERSONAL_LOAN",
        "description":"Empréstimo pessoal sem garantia, até 60 parcelas.",
        "minAmount":{"amount":1000,"currency":"BRL"},
        "maxAmount":{"amount":50000,"currency":"BRL"},
        "minInterestRate":0.0199,"maxInterestRate":0.0450,
        "minInstallments":3,"maxInstallments":60,
        "amortizationMethod":"PRICE"}}}' | jq -r '.data.id')

echo "$NEW"
NEW=$(curl -s -X POST $API/products \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"data":{"type":"products","attributes":{
        "name":"Crédito Pessoal Digital 60x",
        "productType":"PERSONAL_LOAN",
        "description":"Empréstimo pessoal sem garantia, até 60 parcelas.",
        "minAmount":{"amount":1000,"currency":"BRL"},
        "maxAmount":{"amount":50000,"currency":"BRL"},
        "minInterestRate":0.0199,"maxInterestRate":0.0450,
        "minInstallments":3,"maxInstallments":60,
        "amortizationMethod":"PRICE"}}}' | jq -r '.data.id')

echo "$NEW"

Resposta prevista: o identificador do produto novo, já ofertável.

2. Desativar o antigo — NÃO exclua, para não quebrar quem o referencia.

bash
curl -s -X PATCH "$API/products/$PRODUCT_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"active": false}' | jq '.data.attributes.active'
curl -s -X PATCH "$API/products/$PRODUCT_ID" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"active": false}' | jq '.data.attributes.active'

Resposta prevista:

json
false
false

Armadilhas.

  • Desative, não exclua. O produto excluído devolve 404 em GET, e qualquer proposta ou contrato que guarde o productId perde a referência. Desativado continua consultável.
  • O nome é único por organização, então o produto novo precisa de nome diferente enquanto o antigo existir.
  • Contrato já fechado sob o produto antigo continua apontando para o produto antigo — o Products não congela condição no contrato. Se você precisa disso, o BB certo é o Banking Product Portfolio.

Alimentar a calculadora com os parâmetros do produto

Objetivo. Simular parcela e CET a partir do produto cadastrado.

1. Ler os parâmetros do produto

bash
P=$(curl -s "$API/products/$PRODUCT_ID" -H "Authorization: Bearer $TOKEN")

echo "$P" | jq '{
  taxa:  .data.attributes.minInterestRate,
  tac:   .data.attributes.registrationTariffRate,
  iof_a: .data.attributes.iofAdditionalRate,
  iof_d: .data.attributes.iofDailyRate,
  metodo:.data.attributes.amortizationMethod
}'
P=$(curl -s "$API/products/$PRODUCT_ID" -H "Authorization: Bearer $TOKEN")

echo "$P" | jq '{
  taxa:  .data.attributes.minInterestRate,
  tac:   .data.attributes.registrationTariffRate,
  iof_a: .data.attributes.iofAdditionalRate,
  iof_d: .data.attributes.iofDailyRate,
  metodo:.data.attributes.amortizationMethod
}'

Resposta prevista — repare que tac volta null quando não foi informado na criação, e não zero:

json
{
  "taxa": 0.0199,
  "tac": null,
  "iof_a": 0.0038,
  "iof_d": 0.000082,
  "metodo": "PRICE"
}
{
  "taxa": 0.0199,
  "tac": null,
  "iof_a": 0.0038,
  "iof_d": 0.000082,
  "metodo": "PRICE"
}

2. Entregá-los ao Calculations Engine

sequenceDiagram
    autonumber
    participant App as Sua aplicação
    participant PR as Products
    participant PE as Pricing Engine
    participant CE as Calculations Engine

    App->>PR: GET /products/:productId
    PR-->>App: faixa de taxa, TAC, seguro, IOF e método
    App->>PE: pede a taxa por faixa de risco
    PE-->>App: taxa escolhida — precisa caber na faixa do produto
    Note over App: essa checagem NÃO é do Products
    App->>CE: taxa, TAC, seguro, IOF e método
    CE-->>App: cronograma, IOF e CET

Os valores saem daqui e entram na chamada do Calculations Engine, que devolve o cronograma, o IOF e o CET. O Products não calcula nada — ele guarda os parâmetros.

Armadilhas.

  • A taxa que você manda para a calculadora precisa estar dentro da faixa do produto, e essa checagem não é do Products. Quem escolhe a taxa dentro da faixa costuma ser o Pricing Engine.
  • registrationTariffRate e insuranceRate voltam como undefined quando não foram informados na criação — não como zero.
  • Não há chamada do Products para o Calculations Engine. Quem liga os dois é a sua aplicação.

Reagir a mudanças de produto em outro serviço

O Products publica três eventos no fluxo Redis iam-events:

EventoQuandoCarga principal
products.product.createdApós gravarproductId, name, productType, organizationId
products.product.updatedApós atualizarproductId, changes com description e/ou active
products.product.deletedApós exclusão lógicaproductId

Todos carregam metadata.organizationId e metadata.timestamp.

Armadilhas.

  • Com o Redis fora, a escrita falha com 500. É deliberado (§7).
  • Para receber os eventos fora da plataforma, use o Webhooks Engine.

12

Integração com outros building blocks

A fronteira entre os três "catálogos"

A Catalisa tem três building blocks que guardam algo chamado "produto". Eles não competem; resolvem problemas diferentes. Escolher errado custa uma migração.

flowchart TB
    subgraph PR["products — este building block"]
      direction TB
      PR1["<b>É</b>: parâmetros de produto de CRÉDITO<br/>faixa de valor, faixa de taxa, prazo,<br/>TAC, seguro, IOF, método de amortização, LTV"]
      PR2["<b>NÃO é</b>: versão, aprovação, variante,<br/>elegibilidade, estoque, preço de venda, cálculo"]
      PR3["1 entidade · 5 rotas · schema products"]
    end

    subgraph BPP["banking-product-portfolio"]
      direction TB
      BP1["<b>É</b>: catálogo bancário completo — crédito, depósito,<br/>conta, investimento e seguro<br/>com família, variante por canal, versão publicada,<br/>aprovação formal e regra de elegibilidade"]
      BP2["<b>NÃO é</b>: catálogo de varejo, nem calculadora"]
      BP3["5 entidades · 34 rotas"]
    end

    subgraph CM["commerce"]
      direction TB
      CM1["<b>É</b>: produto de VAREJO<br/>SKU, variante, estoque, preço de venda,<br/>lista de preço, carrinho, pedido e canal de venda"]
      CM2["<b>NÃO é</b>: produto de crédito — não tem taxa,<br/>parcela, IOF nem amortização"]
      CM3["32 entidades · 130 rotas"]
    end

    PR -.->|"a mesma família de problema,<br/>em profundidades diferentes.<br/>NÃO há sincronização automática — escolha um"| BPP
    CM -.->|"domínio diferente.<br/>Não competem, não se tocam"| PR
Products (este)Banking Product PortfolioCommerce
O que guardaProduto de crédito, com faixas e encargosCatálogo bancário completo: crédito, depósito, conta, investimento, seguroProduto de varejo: SKU, estoque, preço de venda
Entidades1 (Product)5 (família, produto, variante, versão, elegibilidade)32
Rotas534130
VersionamentoNãoSim, com versão congelada por contratoNão se aplica
Aprovação formalNãoSim, com segregação de funções no códigoNão
Variante por canal ou convênioNãoSimVariação de SKU
Regras de elegibilidadeNãoSimNão
Estoque e preço de vendaNãoNãoSim
Quando usarPoucos produtos de crédito, parâmetros fora do código, sem exigência de versãoMuitas tabelas por canal, exigência de reconstruir a condição de cada contratoOperação que vende mercadoria

Em uma frase: se você vende crédito e precisa só dos parâmetros, é o Products. Se você vende crédito e precisa provar sob que condição cada contrato foi assinado, é o Banking Product Portfolio. Se você vende mercadoria, é o Commerce.

Nota de consistência: o README do Banking Product Portfolio descreve o Products como "catálogo de produtos de varejo com estoque e preço". Isso não confere com o código deste building block, que é de produto de crédito e não tem estoque nem preço de venda. A descrição correta é a desta tabela.

Demais integrações

Building blockComo se relacionaObrigatório
IAMEmite o token com organizationId e as permissões PRODUCTS_*Sim
Calculations EngineRecebe taxa, TAC, seguro, IOF e método para calcular parcela, IOF e CET. A chamada é da sua aplicaçãoNão
Pricing EngineEscolhe a taxa dentro da faixa min/max do produto, por faixa de riscoNão
Decision PlatformUsa valor e prazo pedidos contra as faixas do produto na política de créditoNão
CustomersQuem contrata o produto. Não há chave estrangeira entre os dois — o vínculo é da sua aplicaçãoNão
Audit TrailConsome products.product.* e monta a linha do tempo do produtoNão
Webhooks EngineEntrega os mesmos eventos para fora da plataformaNão
Banking Product PortfolioAlternativa com versionamento. Não há sincronização automática entre os doisNão
flowchart TD
    IAM["IAM<br/>organizationId e permissões"] -->|"Bearer JWT"| P

    P["PRODUCTS<br/>faixas e encargos"]
    PE["Pricing Engine<br/>taxa dentro da faixa"]
    CE["Calculations Engine<br/>parcela · IOF · CET"]
    DP["Decision Platform<br/>aprova ou recusa"]
    CU["Customers<br/>pessoa física — quem contrata"]
    EB["E-Signature e Billing<br/>contrato e cobrança"]

    P -->|parâmetros| PE
    P -->|parâmetros| CE
    PE -->|"taxa escolhida"| DP
    DP --> CE
    DP --> EB
    CU -->|"quem contrata"| EB

Atenção. Todas as setas partindo do Products são chamadas da sua aplicação. O Products não chama nenhum outro building block.

Este diagrama é o argumento comercial: o parâmetro sai de um lugar e alimenta a esteira inteira. Sem ele, cada seta seria uma cópia do mesmo número.


13

Configuração e operação

Variáveis de ambiente

VariávelDescriçãoObrigatóriaPadrão
DATABASE_URLPostgreSQL. O BB usa o schema productsSim—
REDIS_URLRedis. Publicação de eventos e limite de taxaSim—
JWT_SECRETSegredo HS256 do IAM, mínimo 44 caracteresSim—
PORTPorta em modo standaloneNão3000 (a topologia usa 3004)
DEPLOYMENT_MODEmonolith ou standaloneNãomonolith
RATE_LIMIT_ENABLEDLiga o limite global de taxaNãotrue no código; os stacks de staging e produção definem false
RATE_LIMIT_GLOBAL_MAXRequisições por janela, por IPNão10000
CORS_ORIGINSOrigens permitidas, separadas por vírgulaNãovazio

Não há variável específica do Products. Ele não tem chave de cifragem, não fala com provedor externo e não guarda segredo.

Dependências de infraestrutura

DependênciaPara quêSe cair
PostgreSQLSchema products, tabela productsToda rota devolve 500 INTERNAL
RedisPublicação de evento e limite de taxaEscrita falha com 500; leitura continua
IAMEmissão do token. A verificação é localSem token novo; tokens válidos seguem funcionando

Limites

LimiteValorOnde
Tamanho do corpo1 MBapplyCommonMiddleware
Itens por página100getPaginationParams
Nome do produto100 caracteresschema Zod
Descrição5.000 caracteresschema Zod
Parcelas1 a 360schema Zod
Taxa0 a 1 (decimal)schema Zod
Precisão do valor15 dígitos, 2 decimaisDecimal(15,2)
Precisão da taxa10 dígitos, 8 decimaisDecimal(10,8)
Precisão do IOF diário12 dígitos, 10 decimaisDecimal(12,10)

Catálogo de erros

StatusCódigoSignificaO que fazer
400—Corpo reprovado no ZodLeia details[]: field, message e code por problema
400VALIDATIONmin maior que max, LTV fora de MORTGAGE, carência negativaA mensagem diz qual regra falhou
400VALIDATIONConfiguração de amortização ausente ou com type divergenteConfira a tabela da §8
400—productId não é UUIDConfira o identificador
401—Token ausente, inválido ou expiradoRenove pelo refresh token do IAM
403—Falta a permissão PRODUCTS_*Confira papel e permissões da organização no IAM
403—Token sem organizationIdAutentique informando a organização
404NOT_FOUNDProduto inexistente, excluído, ou de outra organizaçãoOs três casos são indistinguíveis — é proposital
409CONFLICTNome já usado na sua organizaçãoEscolha outro nome
429—Limite de taxa por IPRecuo exponencial; use o Retry-After
500INTERNALFalha de banco, ou Redis fora na escritaVerifique Postgres e Redis antes de investigar código

Observabilidade

  • GET /products/health responde status do processo. Não consulta o banco.
  • Todo erro passa pelo errorHandler e sai no log com logger.error({ err }, 'Request error').
  • Cada evento publicado é contabilizado por recordEventPublished, com tipo e building block de origem.
  • O EventPublisher injeta traceId e spanId na metadata do evento, ligando a criação do produto ao registro dela no Audit Trail no mesmo trace.

14

Segurança e compliance

Isolamento entre organizações e permissões

flowchart TD
    Req["Requisição HTTP"] --> A["authMiddleware<br/>verifica a assinatura HS256 localmente"]
    A -->|"sem token válido"| E401["401"]
    A --> B["requirePermission PRODUCTS_CREATE, READ, UPDATE ou DELETE"]
    B -->|"sem a permissão nominal"| E403a["403"]
    B --> C["requireOrganization"]
    C -->|"token sem organizationId"| E403b["403"]
    C --> D["ProductService"]
    D --> R["findById, findMany e nameExists<br/>recebem organizationId como parâmetro obrigatório<br/>e o incluem na cláusula where"]
    R -->|"produto de outra organização"| E404["404 — indistinguível de produto inexistente"]
    R --> OK["200 ou 201"]

Isolamento entre organizações. O organizationId é claim assinado do JWT. As cinco rotas aplicam requireOrganization, que devolve 403 sem o claim. findById, findMany e nameExists recebem organizationId como parâmetro obrigatório e o incluem na cláusula where. Nenhuma rota lê organizationId do corpo, da query ou de cabeçalho. Produto de outra organização devolve 404, igual a produto inexistente.

Autenticação e permissões. Bearer JWT do IAM, HS256 verificado localmente. Cada rota exige uma permissão nominal: PRODUCTS_CREATE, _READ, _UPDATE ou _DELETE. A organização é o teto — papel com PRODUCTS_CREATE numa organização que não contratou o Products não concede nada.

Que dado este BB trata

Que dado este BB trata. Nenhum dado pessoal. O Products guarda condição comercial de produto: nomes, descrições, faixas numéricas e alíquotas. Não há CPF, nome de pessoa, e-mail, telefone nem qualquer informação que identifique um titular. Isso simplifica bastante o enquadramento: a LGPD não incide sobre o conteúdo deste building block, e não há dado pessoal a cifrar, mascarar ou eliminar.

O único ponto de atenção é o campo metadata, que é JSONB livre e não é validado. Se a sua integração gravar dado pessoal ali — CPF de um convênio, nome de um correspondente —, ele passa a existir num building block que não foi projetado para tratá-lo, sem mascaramento e sem política de retenção. Não faça isso.

Confidencialidade comercial e integridade da condição

Confidencialidade comercial. O que o Products guarda é sensível de outro jeito: faixa de taxa, spread possível e política de encargos são informação competitiva. A proteção é a mesma do resto — permissão por rota, isolamento por organização e TLS no transporte. Não há cifragem de campo, e ela não faria sentido aqui: o dado precisa ser lido em toda simulação.

Integridade da condição comercial. O PATCH não altera faixa, taxa, IOF nem método de amortização. Isso é controle de integridade no código: não existe caminho de API que mude a condição de um produto que já está sendo oferecido. O custo é que a alteração vira criação de produto novo, e a rastreabilidade de "qual condição valia quando" depende do Audit Trail estar ligado — confira o estado dele no README correspondente.

Enquadramento regulatório

Enquadramento regulatório. O Products é um repositório de parâmetros, não um sistema regulado. Ele não movimenta recurso, não escritura contrato e não é meio de pagamento. Quando alimenta uma operação de crédito de instituição autorizada, as obrigações de guarda e de rastreabilidade recaem sobre a instituição. Este building block não oferece versionamento nem trilha de aprovação — se o seu enquadramento exige reconstruir a condição vigente em uma data passada, ele não atende, e o Banking Product Portfolio é o bloco desenhado para isso. Não há certificação PCI-DSS, SOC 2 ou ISO 27001 para este building block, e nenhuma deve ser afirmada em proposta.


15

Limitações conhecidas

LimitaçãoImpactoSituação
Sem versionamentoNão há versão publicada, não há snapshot de parâmetros e não há como responder "qual era a taxa deste produto em março". Alteração é destrutivaPor design — é o Banking Product Portfolio
Sem congelamento por contratoContrato assinado aponta para o productId, e o produto pode ser desativado ou excluído. A condição vigente na assinatura não é preservada por este BBPor design
O PATCH só altera description e activeFaixa de valor, taxa, prazo, IOF, seguro, TAC, método de amortização e LTV são imutáveis após a criação. Mudar condição exige produto novoPor design, com custo operacional
Sem família, variante ou canalMesma condição para todos os canais, convênios e correspondentes. Diferenciar exige um produto por combinação, e o nome precisa ser diferente em cada umPor design
Sem aprovação nem segregação de funçõesQuem tem PRODUCTS_CREATE cria produto sozinho. Não há estado de rascunho, não há revisor, não há registro de aprovação dentro do BBNão implementado
Sem regra de elegibilidadeO produto não sabe para quem serve. Idade mínima, renda mínima, convênio e restrição de UF ficam com o Decision PlatformPor design
Sem cálculo financeiroO produto não calcula parcela, IOF nem CET. Ele guarda os parâmetros e o Calculations Engine calculaPor design
gracePeriodMonths e interestOnlyPeriod não são validados um contra o outroÉ possível criar um produto com carência de 6 meses e período só-juros de 12. A coerência é responsabilidade de quem cadastraConhecido
amortizationConfig não é reconferido na leituraO JSONB é gravado como veio e devolvido como está. Um registro alterado direto no banco não é revalidadoConhecido
metadata é JSONB sem validação nem índiceNão filtra, não valida, não é consultável por chave. Serve para carregar atributo próprio, não para estruturar dadoPor design
CUSTOM aceita formula como texto livreO campo é gravado como string e não é interpretado por este BB. Quem entende a fórmula precisa ser o consumidorEspecificado, não implementado
Sem histórico de alteração dentro do BBO que sobra é o changes do evento products.product.updated — que só cobre description e active, e só existe se o consumidor de auditoria estiver ligadoConhecido
Exclusão lógica quebra a referênciaProduto excluído devolve 404 em GET. Quem guardou o productId perde a leitura. Prefira active: falseConhecido
A sonda de saúde não testa o bancoGET /health responde ok com o Postgres fora do arConhecido
Sem importação em massaCada produto é uma chamada POSTRoadmap

16

Perguntas frequentes

Qual a diferença entre o Products e o Banking Product Portfolio?

O Products guarda os parâmetros de um produto de crédito: faixa de valor, faixa de taxa, prazo, encargos e método de amortização. Uma tabela, cinco rotas. O Banking Product Portfolio guarda um catálogo bancário completo, com família, variante por canal, versão publicada, aprovação formal e regra de elegibilidade — cinco tabelas, trinta e quatro rotas. A pergunta que separa os dois é uma só: você precisa reconstruir, no futuro, sob qual condição um contrato foi assinado? Se sim, é o Portfolio. Se não, o Products é mais simples e mais barato de operar.

Então este building block é o de e-commerce?

Não. O Products é de produto de crédito — não tem SKU, estoque, preço de venda, variação de cor nem carrinho. Produto de varejo é o Commerce, que tem 130 rotas e 32 entidades dedicadas a isso. Se alguma documentação da plataforma descreve o Products como catálogo de varejo, está desatualizada; a fonte de verdade é este arquivo e o código em src/products/.

Por que não consigo mudar a taxa de um produto?

Porque o PATCH aceita só description e active. A decisão protege contra alteração acidental de condição de um produto que já está sendo oferecido. O caminho é criar um produto novo com a condição nova e desativar o antigo — com active: false, não com DELETE, para não quebrar quem guardou o productId.

A taxa 0.0199 é 1,99% ou 0,0199%?

É 1,99% ao mês. A taxa é decimal, não percentual, e o schema aceita de 0 a 1. Enviar 1.99 cria um produto com 199% ao mês e passa na validação — é a armadilha de integração mais comum aqui. Confira o valor no retorno do POST.

O Products calcula a parcela?

Não. Ele guarda os parâmetros; quem calcula parcela, IOF e CET é o Calculations Engine, e quem escolhe a taxa dentro da faixa é o Pricing Engine. O Products não chama nenhum dos dois — a orquestração é da sua aplicação.

Duas empresas clientes minhas podem ter produtos com o mesmo nome?

Podem. A unicidade do nome é por organização, checada com o organizationId junto. Isso é diferente do Customers, onde o CPF é único na plataforma inteira.

Como faço um produto diferente por canal ou por convênio?

Criando um produto por combinação, com nome diferente em cada um, e usando metadata para marcar o canal. Não é elegante e não escala: com dez convênios e três canais você tem trinta produtos para manter em sincronia. Se esse é o seu cenário, o Banking Product Portfolio tem variantes de verdade e é o bloco certo.

O que acontece com os contratos quando eu excluo um produto?

Do lado do Products, nada — a exclusão é lógica e o registro permanece na tabela. Do lado da sua aplicação, GET /products/:productId passa a devolver 404, então qualquer tela que resolva o nome do produto a partir do identificador quebra. Use active: false para tirar de circulação sem perder a leitura.

Preciso do Products se já uso o Banking Product Portfolio?

Não. Os dois resolvem a mesma família de problema em profundidades diferentes, e não há sincronização automática entre eles. Escolha um. Manter os dois com o mesmo produto cadastrado nos dois lugares recria exatamente a divergência que este building block existe para eliminar.

Por que Price e SAC são valores do enum, e não uma configuração genérica?

Porque a regulação brasileira os exige pelo nome. A Resolução CMN 4.846/2020, art. 3º, IV, manda apurar saldo devedor e parcelas conforme o Sistema Francês de Amortização (Tabela Price) ou o Sistema de Amortização Constante (SAC), com as bases de cálculo anuais explicitadas. Motores internacionais de produto de crédito trabalham com FLAT e DECLINING_BALANCE — vocabulário que não mapeia um para um no nosso. Guardar o nome certo no produto evita que a tradução vire uma camada que alguém precisa manter.

O que acontece se o Redis cair?

A leitura continua funcionando. A escrita falha com 500, porque a publicação do evento faz parte da cadeia de criação, atualização e exclusão. É deliberado: parâmetro alterado sem evento produz auditoria com buraco.


Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md