Products
ProduçãoParâmetros do seu produto de crédito por API, sem constante escondida no código
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.
- 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
- 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
- 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
6 endpoints em 2 recursos.
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"]| Atributo | Valor |
|---|---|
| Identificador | products |
| Categoria | Financeiro |
| Escopo | Tenant (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 PostgreSQL | products |
| Status | Produção |
| Depende de | PostgreSQL, Redis (publicação de eventos), IAM |
| Permissões | PRODUCTS_CREATE, PRODUCTS_READ, PRODUCTS_UPDATE, PRODUCTS_DELETE |
O problema
negócioO 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.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Faixa de valor e taxa em constante no código | Registro consultável por GET /products/:productId |
| Mudar o teto de prazo exige deploy | Chamada de API por quem tem PRODUCTS_UPDATE |
| Simulador, esteira e calculadora com faixas diferentes | Uma fonte, três consumidores |
Balão e carência viram if no código da esteira | amortizationMethod com configuração validada por método |
| Alíquota de IOF escrita em três lugares | Duas 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.
Casos de uso reais
negócioCaso 1 — O teto de prazo muda numa tarde, não numa sprint Cenário ilustrativo
Fintech de crédito pessoal com três produtos ativos e uma esteira própria de originação.
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.
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"]
endA 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
Operação de crédito com site público que simula parcela, aplicativo que reproduz a simulação e esteira que fecha o contrato.
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.
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"| CEA 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
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.
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.
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"]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
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 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"]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.
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.
Mercado e diferenciais
negócioPanorama: 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ério | Catalisa Products | Constante no código | Mambu Loan Products | Vault Core (Thought Machine) |
|---|---|---|---|---|
| Base de cobrança | Precificação em definição | Engenharia e janela de release | Assinatura, sem preço público | Licença, sem preço público |
| Mudar parâmetro sem deploy | Sim | Não | Sim | Sim |
| Parâmetro consultável por API | Sim | Não | Sim | Sim |
| Faixa de valor, juros e parcelas | Sim | Você implementa | Sim | Você escreve em Python |
| Tabela Price e SAC nominais | Sim | Você implementa | Não | Você implementa |
| IOF adicional e diário | Sim, colunas dedicadas | Você implementa | Não | Você implementa |
| Tarifa de cadastro e seguro | Colunas do produto | Você implementa | Tarifas predefinidas | Você implementa |
| Balão e carência de amortização | Sim, com validação por método | Você implementa | Sim | Você implementa |
| Versão congelada por contrato | Não — é o banking-product-portfolio | Não | Sim | Sim |
| Aprovação formal e segregação de funções | Não | Não | Sim | Sim |
| Variante por canal ou convênio | Não | Não | Sim | Sim |
| Cálculo financeiro embutido | Não — é o calculations-engine | Você implementa | Sim | Sim |
| Adoção sem trocar o core | Sim | Sim | Não | Não |
| Esforço de implantação | Cinco rotas | Zero | Projeto de banco | Projeto 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
- 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. - 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.
- 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 Brasil | Um 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 assinado | Banking Product Portfolio | A 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ções | Banking Product Portfolio | É o bloco desenhado para isso |
| Trocar o core de qualquer jeito | O motor de produto do Mambu ou do Vault Core | Vem 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 ano | A constante no código | Nã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úmero | Catalisa 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.
Modelo de cobrança e ROI
negócioPrecificaçã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.
| Driver | Por quê |
|---|---|
| Produtos ativos no catálogo | É a unidade que o cliente entende e a que cresce com a operação |
| Chamadas de API | Leitura 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 conta | O que se ganha | Por que não colocamos número |
|---|---|---|
| Tempo comercial | O intervalo até a próxima janela de release deixa de existir para alteração de parâmetro | Depende do seu ciclo de release e da sua margem — a conta é sua, e é fácil de fazer |
| Divergência entre canais | Some a simulação que não bate com o contrato | O 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.
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 --> ConsQuem 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 primeiroproductsé obasePathdo app, o segundo é o recurso. Parece erro e não é. Vale conferir na sua integração, porque/products/api/v1sozinho devolve404. - A taxa é guardada como decimal, não como percentual.
minInterestRate: 0.0199significa 1,99% ao mês. O schema aceita de 0 a 1, então uma taxa acima de 100% ao mês é reprovada. Enviar1.99achando 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 campotype, e o serviço confere depois se otypebate com oamortizationMethod. A dupla checagem é redundante de propósito: a primeira dá erro de campo legível, a segunda impede combinação inconsistente. PRICEeSACnão aceitam configuração e não precisam.validateAmortizationConfigretorna cedo para os dois. MandaramortizationConfigjunto deamortizationMethod: "PRICE"é reprovado, porque otypenão pode casar.- O
PATCHaltera apenasdescriptioneactive. 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.
deletedAtpreenchido, linha preservada, some de todas as consultas. Contrato antigo que referencie oproductIdcontinua conseguindo... nada — oGETdevolve404. 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.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Product | Um produto de crédito de uma organização, com faixas e encargos. É a única entidade do BB. |
| Faixa | Todo parâmetro comercial é um par mínimo/máximo: valor, taxa e parcelas. A esteira escolhe dentro da faixa; o produto define o limite. |
PRICE | Sistema 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. |
SAC | Sistema 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 decimal | 0.0199 é 1,99% ao mês. Aceita de 0 a 1. Nunca em pontos percentuais. |
| IOF adicional | Alíquota única incidente sobre o valor da operação. Coluna iofAdditionalRate. |
| IOF diário | Alí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. |
| LTV | Loan-to-Value, a razão entre o empréstimo e o valor do bem. Aceito somente em produto do tipo MORTGAGE. |
metadata | JSONB livre para atributo próprio da organização. Não é validado, não é indexado, não é filtrável. |
| Exclusão lógica | deletedAt 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 Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
Product | products.products | Produto de crédito com faixas e encargos | organizationId, name (único por organização, validado no serviço), productType, amortizationMethod, active, deletedAt |
Campos por grupo:
| Grupo | Colunas | Tipo | Obrigatório |
|---|---|---|---|
| Identificação | name, productType, description, active | texto / enum / texto / booleano | Sim (active tem padrão true) |
| Faixa de valor | minAmount, maxAmount | Decimal(15,2) | Sim |
| Faixa de taxa | minInterestRate, maxInterestRate | Decimal(10,8) | Sim |
| Faixa de prazo | minInstallments, maxInstallments | inteiro, 1 a 360 | Sim |
| Encargos | registrationTariffRate, insuranceRate | Decimal(10,8) | Não |
| IOF | iofAdditionalRate Decimal(10,8), iofDailyRate Decimal(12,10) | decimal | Não |
| Amortização | amortizationMethod (padrão PRICE), amortizationConfig JSONB, gracePeriodMonths | — | Não |
| Garantia | minLtv, maxLtv | Decimal(5,4) | Não, e só em MORTGAGE |
| Extensão | metadata | JSONB | Não |
| Controle | createdAt, updatedAt, deletedAt | timestamp | Automá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
| Enum | Valores |
|---|---|
ProductType | PERSONAL_LOAN · PAYROLL_LOAN · VEHICLE_LOAN · HOME_EQUITY · CREDIT_CARD · WORKING_CAPITAL · INVOICE_FINANCING · MORTGAGE |
AmortizationMethod | PRICE · SAC · BALLOON · BULLET · INTEREST_ONLY · STEP_UP · STEP_DOWN · CUSTOM |
Uma versão anterior desta documentação listava
ProductTypecom os valoresLOANeCREDIT_LINE, eAmortizationMethodsó comSACePRICE. Nenhum dos dois confere. A fonte ésrc/products/types/index.ts.
Configuração exigida por método de amortização
| Método | amortizationConfig | Campos | Validação |
|---|---|---|---|
PRICE | Não aceita | — | Parcela fixa. Configuração é ignorada pela validação e reprovada pelo casamento de type |
SAC | Não aceita | — | Amortização constante |
BALLOON | Obrigatória | balloonPercentage | De 0,01 a 0,99 |
BULLET | Opcional | — | Principal e juros no vencimento |
INTEREST_ONLY | Obrigatória | interestOnlyPeriod, postGraceMethod | Período ≥ 1; método posterior é PRICE ou SAC |
STEP_UP | Obrigatória | stepPercentage, stepInterval | Percentual de 0,01 a 0,50; intervalo ≥ 1 |
STEP_DOWN | Obrigatória | stepPercentage, stepInterval | Mesmas regras do STEP_UP |
CUSTOM | Obrigatória | paymentSchedule ou formula | Ao menos um dos dois |
PRICEeSACsão os únicos que dispensam configuração. Otypedentro deamortizationConfigprecisa ser igual aoamortizationMethod, senão a criação devolve400.
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 noteTrês ausências que o diagrama torna visíveis, e que são a razão de existir o Banking Product Portfolio:
| Não existe | Consequência |
|---|---|
Estado rascunho, publicado, aprovado ou depreciado | Quem tem PRODUCTS_CREATE cria produto sozinho, e ele já nasce ofertável |
| Versão | Mudar condição = criar produto novo + desativar o antigo |
| Congelamento por contrato | Quem precisa de versão publicada e aprovação usa o banking-product-portfolio |
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étodo | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /products/api/v1/products | Cria produto. Responde 201 | PRODUCTS_CREATE |
GET | /products/api/v1/products | Lista produtos, paginado e filtrável | PRODUCTS_READ |
GET | /products/api/v1/products/:productId | Busca um produto | PRODUCTS_READ |
PATCH | /products/api/v1/products/:productId | Atualiza description e/ou active | PRODUCTS_UPDATE |
DELETE | /products/api/v1/products/:productId | Exclusão lógica. Responde 204 sem corpo | PRODUCTS_DELETE |
Saúde do serviço
| Método | Rota | Descrição |
|---|---|---|
GET | /products/health | Status 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
{
"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 }
}
}
}| Campo | Tipo | Obrigatório | Regra |
|---|---|---|---|
name | string | Sim | 1 a 100 caracteres. Único dentro da sua organização |
productType | enum | Sim | Ver §8 |
description | string | Sim | 1 a 5.000 caracteres |
active | boolean | Não | Padrão true |
minAmount.amount | number | Sim | Positivo. min ≤ max |
maxAmount.amount | number | Sim | Positivo |
minInterestRate | number | Sim | De 0 a 1. Decimal, não percentual |
maxInterestRate | number | Sim | De 0 a 1. min ≤ max |
minInstallments | inteiro | Sim | De 1 a 360. min ≤ max |
maxInstallments | inteiro | Sim | De 1 a 360 |
registrationTariffRate | number | Não | De 0 a 1 |
insuranceRate | number | Não | De 0 a 1 |
iofAdditionalRate | number | Não | De 0 a 1 |
iofDailyRate | number | Não | De 0 a 1 |
amortizationMethod | enum | Não | Padrão PRICE |
amortizationConfig | objeto | Depende do método | Ver a tabela da §8 |
gracePeriodMonths | inteiro | Não | ≥ 0 |
minLtv / maxLtv | number | Não | De 0 a 1. Só em MORTGAGE; minLtv ≤ maxLtv |
metadata | objeto | Não | JSON livre, sem validação |
Resposta 201
{
"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
| Status | Código | Quando |
|---|---|---|
400 | — | Corpo reprovado no Zod. details[] traz field, message e code |
400 | VALIDATION | min maior que max em valor, taxa ou parcelas |
400 | VALIDATION | amortizationConfig.type diferente do amortizationMethod |
400 | VALIDATION | Método que exige configuração enviado sem ela |
400 | VALIDATION | minLtv/maxLtv em produto que não é MORTGAGE |
400 | VALIDATION | gracePeriodMonths negativo |
401 | — | Token ausente, inválido ou expirado |
403 | — | Falta PRODUCTS_CREATE, ou o token não carrega organizationId |
409 | CONFLICT | Já existe produto com esse nome na sua organização |
GET /products/api/v1/products
Parâmetros de consulta
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
page[number] | inteiro | 1 | Página, mínimo 1 |
page[size] | inteiro | 20 | Itens 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.
{ "active": false }{ "active": false }| Campo | Tipo | Regra |
|---|---|---|
description | string | 1 a 5.000 caracteres |
active | boolean | — |
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.
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
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
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}'{ "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
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:
{ "name": "Crédito Pessoal Digital", "productType": "PERSONAL_LOAN", "maxInstallments": 48 }{ "name": "Crédito Pessoal Digital", "productType": "PERSONAL_LOAN", "maxInstallments": 48 }4. Ler um produto
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
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.
Receitas
Criar um produto com balão
Objetivo. Produto com parcela final ampliada de 30% do principal.
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:
{ "type": "BALLOON", "config": { "balloonPercentage": 0.3 } }{ "type": "BALLOON", "config": { "balloonPercentage": 0.3 } }Armadilhas.
- O
typedentro deamortizationConfigprecisa ser idêntico aoamortizationMethod.BALLOONcomtype: "BULLET"devolve400. balloonPercentageé decimal de 0,01 a 0,99.30é reprovado;0.30é o certo.- Omitir
amortizationConfignum método que a exige devolve400com a mensagem do método.
Criar um produto com carência de amortização
Objetivo. Seis meses pagando só juros, depois Price.
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.
c7f0a3e8-1b44-4d02-9a51-6e83f0b1c227c7f0a3e8-1b44-4d02-9a51-6e83f0b1c227flowchart 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.
gracePeriodMonthseinterestOnlyPeriodsão campos diferentes e não são validados um contra o outro. Manter os dois coerentes é responsabilidade sua.postGraceMethodaceita sóPRICEouSAC.- 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.
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.
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:
falsefalseArmadilhas.
- Desative, não exclua. O produto excluído devolve
404emGET, e qualquer proposta ou contrato que guarde oproductIdperde 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
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:
{
"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 CETOs 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.
registrationTariffRateeinsuranceRatevoltam comoundefinedquando 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:
| Evento | Quando | Carga principal |
|---|---|---|
products.product.created | Após gravar | productId, name, productType, organizationId |
products.product.updated | Após atualizar | productId, changes com description e/ou active |
products.product.deleted | Após exclusão lógica | productId |
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.
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 Portfolio | Commerce | |
|---|---|---|---|
| O que guarda | Produto de crédito, com faixas e encargos | Catálogo bancário completo: crédito, depósito, conta, investimento, seguro | Produto de varejo: SKU, estoque, preço de venda |
| Entidades | 1 (Product) | 5 (família, produto, variante, versão, elegibilidade) | 32 |
| Rotas | 5 | 34 | 130 |
| Versionamento | Não | Sim, com versão congelada por contrato | Não se aplica |
| Aprovação formal | Não | Sim, com segregação de funções no código | Não |
| Variante por canal ou convênio | Não | Sim | Variação de SKU |
| Regras de elegibilidade | Não | Sim | Não |
| Estoque e preço de venda | Não | Não | Sim |
| Quando usar | Poucos produtos de crédito, parâmetros fora do código, sem exigência de versão | Muitas tabelas por canal, exigência de reconstruir a condição de cada contrato | Operaçã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 block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token com organizationId e as permissões PRODUCTS_* | Sim |
| Calculations Engine | Recebe taxa, TAC, seguro, IOF e método para calcular parcela, IOF e CET. A chamada é da sua aplicação | Não |
| Pricing Engine | Escolhe a taxa dentro da faixa min/max do produto, por faixa de risco | Não |
| Decision Platform | Usa valor e prazo pedidos contra as faixas do produto na política de crédito | Não |
| Customers | Quem contrata o produto. Não há chave estrangeira entre os dois — o vínculo é da sua aplicação | Não |
| Audit Trail | Consome products.product.* e monta a linha do tempo do produto | Não |
| Webhooks Engine | Entrega os mesmos eventos para fora da plataforma | Não |
| Banking Product Portfolio | Alternativa com versionamento. Não há sincronização automática entre os dois | Nã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"| EBAtençã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.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATABASE_URL | PostgreSQL. O BB usa o schema products | Sim | — |
REDIS_URL | Redis. Publicação de eventos e limite de taxa | Sim | — |
JWT_SECRET | Segredo HS256 do IAM, mínimo 44 caracteres | Sim | — |
PORT | Porta em modo standalone | Não | 3000 (a topologia usa 3004) |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
RATE_LIMIT_ENABLED | Liga o limite global de taxa | Não | true no código; os stacks de staging e produção definem false |
RATE_LIMIT_GLOBAL_MAX | Requisições por janela, por IP | Não | 10000 |
CORS_ORIGINS | Origens permitidas, separadas por vírgula | Não | vazio |
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ência | Para quê | Se cair |
|---|---|---|
| PostgreSQL | Schema products, tabela products | Toda rota devolve 500 INTERNAL |
| Redis | Publicação de evento e limite de taxa | Escrita falha com 500; leitura continua |
| IAM | Emissão do token. A verificação é local | Sem token novo; tokens válidos seguem funcionando |
Limites
| Limite | Valor | Onde |
|---|---|---|
| Tamanho do corpo | 1 MB | applyCommonMiddleware |
| Itens por página | 100 | getPaginationParams |
| Nome do produto | 100 caracteres | schema Zod |
| Descrição | 5.000 caracteres | schema Zod |
| Parcelas | 1 a 360 | schema Zod |
| Taxa | 0 a 1 (decimal) | schema Zod |
| Precisão do valor | 15 dígitos, 2 decimais | Decimal(15,2) |
| Precisão da taxa | 10 dígitos, 8 decimais | Decimal(10,8) |
| Precisão do IOF diário | 12 dígitos, 10 decimais | Decimal(12,10) |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | — | Corpo reprovado no Zod | Leia details[]: field, message e code por problema |
400 | VALIDATION | min maior que max, LTV fora de MORTGAGE, carência negativa | A mensagem diz qual regra falhou |
400 | VALIDATION | Configuração de amortização ausente ou com type divergente | Confira a tabela da §8 |
400 | — | productId não é UUID | Confira o identificador |
401 | — | Token ausente, inválido ou expirado | Renove pelo refresh token do IAM |
403 | — | Falta a permissão PRODUCTS_* | Confira papel e permissões da organização no IAM |
403 | — | Token sem organizationId | Autentique informando a organização |
404 | NOT_FOUND | Produto inexistente, excluído, ou de outra organização | Os três casos são indistinguíveis — é proposital |
409 | CONFLICT | Nome já usado na sua organização | Escolha outro nome |
429 | — | Limite de taxa por IP | Recuo exponencial; use o Retry-After |
500 | INTERNAL | Falha de banco, ou Redis fora na escrita | Verifique Postgres e Redis antes de investigar código |
Observabilidade
GET /products/healthresponde status do processo. Não consulta o banco.- Todo erro passa pelo
errorHandlere sai no log comlogger.error({ err }, 'Request error'). - Cada evento publicado é contabilizado por
recordEventPublished, com tipo e building block de origem. - O
EventPublisherinjetatraceIdespanIdna metadata do evento, ligando a criação do produto ao registro dela no Audit Trail no mesmo trace.
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.
Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
| Sem versionamento | Nã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 é destrutiva | Por design — é o Banking Product Portfolio |
| Sem congelamento por contrato | Contrato 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 BB | Por design |
O PATCH só altera description e active | Faixa 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 novo | Por design, com custo operacional |
| Sem família, variante ou canal | Mesma 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 um | Por design |
| Sem aprovação nem segregação de funções | Quem 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 BB | Não implementado |
| Sem regra de elegibilidade | O produto não sabe para quem serve. Idade mínima, renda mínima, convênio e restrição de UF ficam com o Decision Platform | Por design |
| Sem cálculo financeiro | O produto não calcula parcela, IOF nem CET. Ele guarda os parâmetros e o Calculations Engine calcula | Por 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 cadastra | Conhecido |
amortizationConfig não é reconferido na leitura | O JSONB é gravado como veio e devolvido como está. Um registro alterado direto no banco não é revalidado | Conhecido |
metadata é JSONB sem validação nem índice | Não filtra, não valida, não é consultável por chave. Serve para carregar atributo próprio, não para estruturar dado | Por design |
CUSTOM aceita formula como texto livre | O campo é gravado como string e não é interpretado por este BB. Quem entende a fórmula precisa ser o consumidor | Especificado, não implementado |
| Sem histórico de alteração dentro do BB | O que sobra é o changes do evento products.product.updated — que só cobre description e active, e só existe se o consumidor de auditoria estiver ligado | Conhecido |
| Exclusão lógica quebra a referência | Produto excluído devolve 404 em GET. Quem guardou o productId perde a leitura. Prefira active: false | Conhecido |
| A sonda de saúde não testa o banco | GET /health responde ok com o Postgres fora do ar | Conhecido |
| Sem importação em massa | Cada produto é uma chamada POST | Roadmap |
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