Document Template
BetaContratos e documentos gerados a partir de template versionado, com variáveis tipadas
O contrato que o seu cliente assina para de morar dentro do código do seu sistema. Vira um template versionado, que o jurídico edita, publica e consegue provar, dois anos depois, exatamente qual redação foi usada naquela operação.
- Financeiras e fintechs de crédito que emitem CCB, contrato e aditivo em cada operação
- Operações reguladas que precisam provar qual versão do contrato o cliente aceitou
- Times de produto que hoje montam HTML de contrato dentro do código da aplicação
- Serviço externo de geração de PDF por API cobrado por documento
- Template de contrato colado dentro do código-fonte da aplicação
- Planilha ou pasta compartilhada com as versões do contrato em Word
- Um editor visual de documento para o usuário final (não há interface, é API)
- Uma ferramenta de assinatura eletrônica (isso é o e-signature)
- Um gerador de relatório analítico ou de dashboard
12 endpoints em 3 recursos.
Resumo executivo
O Document Template guarda o texto dos seus documentos — contrato, CCB, proposta, recibo — fora do código, com marcadores no lugar dos dados que mudam a cada operação. Você envia os valores, ele devolve o documento pronto.
Na prática, resolve o dia em que o jurídico manda trocar uma cláusula do contrato de crédito. Sem ele, isso vira ticket de engenharia, deploy e a dúvida de quais contratos já emitidos usaram a redação antiga. Com ele, o texto é editado, publicado como uma nova versão, e todo documento emitido guarda o número da versão que o gerou — a pergunta "qual redação esse cliente assinou" vira uma consulta.
Está em beta desde janeiro de 2026. Roda no monolito e em standalone na porta 3019, e está declarado nas pilhas de staging e de produção. A saída em HTML funciona; a saída em PDF depende de um serviço acompanhante que não está publicado em nenhum ambiente — leia a §15 antes de prometer PDF a cliente.
| Atributo | Valor |
|---|---|
| Identificador | document-template |
| Categoria | Documentos |
| Escopo | Tenant (exige organizationId no token, em todas as rotas) |
| Porta (standalone) | 3019 |
| Path alias | @document-template |
| Prefixo HTTP | /document-templates |
| Status | Beta desde 2026-01 |
| Depende de | PostgreSQL (schema doctemplates), Redis, serviço pdf-renderer (opcional) |
| Permissões | DOCUMENT_TEMPLATES_CREATE, _READ, _UPDATE, _DELETE, _PUBLISH, _RENDER |
O problema
negócioO cenário. Uma financeira fecha uma operação de crédito. No fim da esteira, alguém precisa emitir a Cédula de Crédito Bancário com o nome do devedor, o CPF, o valor principal, a taxa, o CET, o número de parcelas, o IOF e a data do primeiro vencimento. O documento tem que sair correto, com a redação vigente, e tem que continuar comprovável depois.
O que trava hoje.
- O contrato mora no código. O HTML do documento fica dentro do repositório da aplicação. Trocar uma vírgula de cláusula exige pull request, revisão, build e deploy — para uma mudança que o jurídico deveria conseguir fazer sozinho.
- Ninguém sabe qual versão foi usada. O código só tem a versão de hoje. Se o contrato mudou três vezes no ano, descobrir qual texto o cliente de março recebeu significa procurar no histórico do Git e torcer para a data do deploy bater com a data da operação.
- Campo em branco vira contrato inválido. Sem validação do conjunto de variáveis, um dado ausente na esteira imprime
undefined, ou pior, imprime um espaço vazio no meio de uma cláusula financeira e ninguém percebe. - Cada time reinventa a mesma coisa. Um serviço monta HTML por concatenação, outro usa uma biblioteca de template, um terceiro chama um serviço externo. Três formas de errar e nenhuma trilha comum.
- O serviço externo leva o dado do contrato para fora. Enviar nome, CPF, valor e condições de crédito a um conversor de PDF de terceiro é uma transferência de dado pessoal que precisa de base legal e de contrato de operador. Costuma ser feito sem nenhum dos dois.
O custo de não resolver. O custo visível é a fila de engenharia para mudar texto jurídico. O custo caro aparece depois: numa disputa ou numa fiscalização, "não conseguimos reproduzir exatamente o documento que foi emitido naquela data" é uma resposta que custa mais do que qualquer sprint. A CCB é título executivo extrajudicial por força do art. 28 da Lei nº 10.931/2004, e o art. 29 lista seis requisitos essenciais de forma. Um documento gerado sem controle de versão é um documento que você não consegue defender — e um requisito essencial que saiu em branco é um título cuja executividade alguém vai questionar.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| O texto do contrato está dentro do código da aplicação | O texto é um recurso versionado, editável sem deploy |
| "Qual redação o cliente de março aceitou?" é investigação no Git | Cada documento emitido carrega o número da versão que o gerou |
| Dado faltando imprime campo vazio no contrato | Variável obrigatória ausente devolve 400 antes de gerar qualquer coisa |
| Cada serviço monta documento do seu jeito | Um catálogo de templates, uma engine, um formato de saída |
| O contrato trafega para um conversor de PDF de terceiro | O conteúdo não sai da sua infraestrutura |
Publicar congela a versão. POST /publish copia o conteúdo e a declaração de variáveis para uma linha imutável em document_template_versions. Editar o template depois muda o rascunho de trabalho, não a versão publicada. Renderização sempre usa a última versão publicada — nunca o rascunho.
A variável tem tipo e é conferida. Cada template declara suas variáveis com nome, tipo (STRING, NUMBER, BOOLEAN, DATE, ARRAY, OBJECT), obrigatoriedade e valor padrão. Antes de renderizar, o serviço confere presença e tipo. Falta de dado é erro de validação, não é contrato com buraco.
Toda emissão vira registro. Cada renderização grava uma linha em rendered_documents com os valores de entrada, o HTML produzido, a versão usada e o identificador do negócio. É a diferença entre saber que um contrato foi gerado e conseguir reproduzi-lo.
O documento nasce dentro da operação. O campo businessId amarra o documento à proposta, ao pedido ou ao contrato que o originou, e o organizationId vem do token. O documento gerado já sabe de quem é e a que operação pertence.
Casos de uso reais
negócioOs quatro casos abaixo cobrem os três motivos pelos quais uma operação adota este building block: tirar o texto jurídico do código, impedir que dado faltando vire contrato com buraco, e reunir documentos diferentes num catálogo só. O quarto é uma leitura do mercado brasileiro, não um cliente.
flowchart LR C1["Caso 1 · redação muda sem deploy"] --> D["Document Template"] C2["Caso 2 · contrato não sai com campo vazio"] --> D C3["Caso 3 · proposta, CCB e recibo no mesmo catálogo"] --> D D --> R["Documento emitido, com versão registrada"]
Caso 1 — Uma financeira muda cláusula de CCB sem pedir deploy Cenário ilustrativo
Financeira de crédito pessoal que emite entre 800 e 1.200 CCBs por mês, cada uma com valor, taxa, CET, IOF e cronograma calculados na esteira.
O texto da cédula estava dentro do código do serviço de originação. Toda alteração pedida pelo jurídico — inclusão da cláusula de portabilidade, ajuste na redação da mora — virava um ciclo de duas semanas entre chamado, desenvolvimento, revisão e janela de deploy. E como o código só guarda a versão atual, ninguém conseguia responder com segurança qual texto tinha sido emitido em determinada data.
A cédula vira um template com quinze variáveis declaradas (valor_principal, taxa_mensal, cet_anual, num_parcelas, iof_total, sistema_amortizacao, garantias, entre outras). O jurídico revisa o conteúdo, o time chama PATCH no template e depois POST .../publish com o changelog da mudança. A esteira continua chamando POST .../render com os valores calculados; ela nem sabe que a redação mudou.
flowchart LR J["Jurídico revisa a redação"] --> P["PATCH no template (rascunho)"] P --> PV["POST de publish com changelog"] PV --> V["Versão imutável nova"] E["Esteira de originação"] --> RD["POST de render, sempre a última versão"] V --> RD RD --> DOC["CCB emitida, com o número da versão gravado"]
Mudança de cláusula deixa de exigir deploy. E GET .../versions responde, em uma chamada, quais versões existiram, quando cada uma foi publicada e o que mudou em cada uma.
sequenceDiagram autonumber participant App as Esteira de crédito participant DT as Document Template participant FS as File Storage participant ES as E-Signature App->>DT: "POST de render/pdf com as variáveis da CCB e o businessId" DT-->>App: "PDF em base64 e o templateVersionNumber" App->>FS: "POST de arquivo com o PDF" FS-->>App: "fileId" App->>ES: "POST de solicitação de assinatura com fileId, signatários e businessId" ES-->>App: "id da solicitação e viewUrl para o signatário"
Caso 2 — A esteira para de emitir contrato com campo vazio Cenário ilustrativo
Operação de crédito consignado em que o valor do IOF é calculado por um serviço separado e chega assíncrono à etapa de emissão do contrato.
Em uma fração das operações o cálculo de IOF atrasava e o contrato saía com o campo em branco. O problema só era detectado quando o cliente reclamava — ou, pior, quando não reclamava. Cada ocorrência exigia cancelar o contrato, reemitir e refazer a coleta de assinatura.
O template declara iof_total como NUMBER obrigatório. Se o valor não chega, POST .../render devolve 400 com Required variable 'iof_total' is missing e nenhum documento é gerado, nenhuma linha é gravada e nenhuma assinatura é solicitada. A esteira trata o 400 como "ainda não pronto" e reprocessa.
flowchart LR
A["Esteira chama o render com as variáveis"] --> B{"iof_total presente e numérico?"}
B -->|Não| C["400 · Required variable iof_total is missing"]
C --> D["Nada é gerado, nada é gravado, nada é assinado"]
D --> E["Esteira aguarda o cálculo e tenta de novo"]
E --> A
B -->|Sim| F["Contrato completo, emissão registrada"]A classe inteira de erro deixa de existir na saída. O custo se move de "reemitir contrato assinado" para "esperar mais trinta segundos".
Caso 3 — Documentos diferentes, uma cadeia só Cenário ilustrativo
Fintech que emite, no mesmo fluxo, a proposta comercial, a CCB e o recibo de liberação.
Cada documento tinha sido resolvido de um jeito em um momento diferente: a proposta era montada no front, a CCB no backend, e o recibo vinha de uma planilha exportada. Três formatos, três lugares para mudar layout, e nenhuma visão de quantos documentos a operação emitia.
Os três viram templates com categorias distintas (PROPOSAL, CONTRACT, RECEIPT) dentro do mesmo catálogo. A emissão é a mesma chamada em todos os casos, mudando só o templateId e as variáveis. O businessId é o mesmo nos três, então listar tudo que foi emitido para uma operação é filtrar por um campo.
flowchart LR OP["Operação · businessId proposta-2026-000481"] --> T1["Template PROPOSAL"] OP --> T2["Template CONTRACT · a CCB"] OP --> T3["Template RECEIPT"] T1 --> R["Mesma chamada de render, só muda o templateId"] T2 --> R T3 --> R R --> L["Três emissões com o mesmo businessId"]
Um catálogo, uma forma de emitir, uma consulta que responde "o que já foi gerado para esta proposta".
Caso 4 — Quem emite CCB no Brasil resolve o template com gente, não com produto Referência de mercado
A Qi Tech é uma das plataformas brasileiras mais transparentes sobre emissão de dívida, e publica documentação aberta do fluxo de emissão de CCB, CCI e CCE (Qi Tech — emissão de dívida, consulta em 2026-08-16).
A dor aqui é do mercado inteiro, não de um cliente. A própria documentação descreve o caminho: "com a minuta da operação de crédito definida, o time de suporte QI Tech realizará a parametrização do contrato na plataforma e seu PDF poderá ser emitido". Ou seja, o cliente envia a minuta e um time humano parametriza o contrato dentro da plataforma — não há upload de template pelo cliente, nem linguagem de template exposta, nem autonomia para mudar uma cláusula. Cada revisão jurídica vira um pedido ao fornecedor. Na Celcoin, a CCB aparece nomeada como produto da linha de crédito (celcoin.com.br, consulta em 2026-08-16), sem detalhamento público do mecanismo de geração do documento.
flowchart LR
subgraph MODELO_FORNECEDOR["Modelo do fornecedor"]
A1["Você envia a minuta"] --> A2["Time humano parametriza o contrato"]
A2 --> A3["PDF emitido pela plataforma"]
A4["Nova revisão jurídica"] --> A1
end
subgraph MODELO_CATALISA["Modelo Catalisa"]
B1["Você cria e edita o template por API"] --> B2["POST de publish com changelog"]
B2 --> B3["Documento emitido, versão registrada"]
B4["Nova revisão jurídica"] --> B1
endComo a Catalisa endereça: o template é um recurso da sua organização, que você cria, edita, publica e versiona por API, sem intermediário. A revisão do jurídico vira PATCH mais POST /publish, com changelog registrado, e não um chamado com prazo de fornecedor.
A autonomia sobre a redação do contrato fica com quem responde juridicamente por ela. Vale registrar a leitura honesta do mercado: nenhum dos fornecedores brasileiros que conseguimos verificar publica motor de template self-service para contrato de crédito — o que pode significar espaço em aberto ou risco que o mercado deliberadamente não quis assumir. Não temos evidência pública para decidir qual das duas.
Mercado e diferenciais
negócioPanorama — os três caminhos de hoje
Panorama. Quem precisa gerar documento hoje escolhe entre três caminhos. O primeiro é um serviço por API — DocRaptor, PDFMonkey, Docmosis, Carbone —, que resolve a conversão muito bem e cobra por documento, por página ou por faixa de volume. O segundo é fazer em casa com WeasyPrint ou Puppeteer, o que elimina a licença e cria uma operação. O terceiro, mais comum do que se admite, é montar HTML dentro do código da aplicação e imprimir.
flowchart TD N["Preciso gerar contrato a partir de dados"] --> C1["Serviço por API"] N --> C2["Renderizador em casa"] N --> C3["HTML montado dentro do código"] C1 --> P1["Cobra por documento, página ou faixa"] C2 --> P2["Sem licença, com operação a manter"] C3 --> P3["Sem custo aparente, e sem trilha nenhuma"] P1 --> L["Nenhum dos três prova qual redação foi usada"] P2 --> L P3 --> L
O wkhtmltopdf saiu da mesa
Um aviso útil para quem está escolhendo: o wkhtmltopdf saiu da mesa. O repositório foi arquivado em janeiro de 2023 e está em modo somente leitura (wkhtmltopdf no GitHub, consulta em 2026-08-16). Ele roda sobre um fork do QtWebKit baseado em Qt 4.8 — um motor de navegador descontinuado há mais de uma década, sem CSS moderno e sem correção de segurança. Se a sua avaliação ainda o inclui, exclua.
A lacuna que o Document Template ocupa
Nenhum dos três caminhos resolve o que dói de verdade em operação regulada: provar qual redação foi usada em qual documento. Essa é a lacuna que o Document Template ocupa. Ele não compete em fidelidade tipográfica com o PrinceXML do DocRaptor; compete em ciclo de vida do documento dentro de uma plataforma multi-tenant.
Comparativo lado a lado
| Critério | Catalisa Document Template | DocRaptor | Carbone.io | Docmosis | PDFMonkey | WeasyPrint em casa |
|---|---|---|---|---|---|---|
| Preço público (2026-08-16) | Em definição | US$ 15 a US$ 1.000/mês por faixa | US$ 29 a US$ 595/mês; US$ 1.500 a US$ 2.940/ano auto-hospedado | US$ 49 a US$ 78/mês; US$ 3.795 a US$ 17.145 perpétua | € 5 a € 300/mês | Sem licença |
| Formato do template | HTML com Handlebars | HTML/CSS | DOCX, ODT, XLSX | DOCX, ODT | HTML com Liquid | O que você escrever |
| Editado por não-engenheiro | Não (é API; o conteúdo é HTML) | Não | Sim, no Word | Sim, no Word | Parcial, no painel | Não |
| Versionamento imutável com changelog | Sim, nativo | Não | Não | Não | Não | Você implementa |
| Variáveis tipadas e validadas | Sim, recusa dado faltando | Não | Não | Não | Não | Você implementa |
| Histórico de emissão consultável | Sim, com valores de entrada | Não | Não | Não | Parcial | Você implementa |
| Isolamento multi-tenant | Sim, pelo token do IAM | Você implementa | Você implementa | Você implementa | Você implementa | Você implementa |
| Saída em PDF | Sim, via serviço acompanhante (ver §15) | Sim | Sim | Sim | Sim | Sim |
| Motor de renderização | O que você acoplar | PrinceXML | LibreOffice headless | LibreOffice | Chrome | WeasyPrint |
| CSS Paged Media completo | Depende do acoplado | Sim | Parcial | Parcial | Não | Sim |
| Saída em DOCX | Não | Não | Sim | Sim | Não | Não |
| O dado sai da sua infraestrutura | Não | Sim | Sim na nuvem, não no auto-hospedado | Sim na nuvem, não na perpétua | Sim | Não |
Preços consultados nas páginas oficiais em 2026-08-16: DocRaptor (a URL
/pricingretorna 404; a tabela está no cadastro), Carbone, Docmosis e PDFMonkey. Todos variam por faixa, região e negociação — confira na data da sua análise. Os valores são dos fornecedores, não estimativas nossas.
Nossos diferenciais
Nossos diferenciais
- Publicação congela conteúdo e contrato de dados juntos. A versão publicada guarda o HTML e a declaração de variáveis daquele momento. Reproduzir um documento antigo não depende de lembrar quais campos existiam na época — a versão sabe. É difícil de copiar não pela mecânica, mas porque exige tratar o documento como recurso versionado desde o primeiro dia, e não como string.
- A validação acontece antes da geração. Variável obrigatória ausente, tipo errado ou data inválida param a requisição com
400. Um conversor de HTML em PDF não tem como fazer isso: para ele, o HTML já chega pronto, com o buraco dentro. - A emissão é prova, não log.
rendered_documentsguarda os valores de entrada, a versão usada, o HTML gerado e o vínculo com a operação. É o que permite responder a um questionamento sem depender de backup. - O tenant vem do token e nunca do corpo. Templates e documentos são isolados por organização no banco. Em serviço externo, isolamento entre clientes é problema seu — e problema que só aparece no dia do incidente.
Quando escolher o concorrente
Quando escolher o concorrente. Cinco situações em que a resposta honesta não é o Document Template. Cada linha traz o requisito, para quem ele aponta e o motivo.
| Se o seu requisito é | Escolha | Por quê |
|---|---|---|
| Tipografia de impressão de alta exigência — cabeçalho e rodapé correntes de verdade, sumário com número de página, controle de viúvas e órfãs | DocRaptor com PrinceXML | Entrega hoje uma qualidade que um Chrome headless não alcança, e o Document Template depende do renderizador que você acoplar |
| Que o jurídico edite o template sozinho no Word | Carbone e Docmosis | Resolvem isso e nós não: aqui o conteúdo é HTML, e HTML não é ferramenta de advogado |
| Saída em DOCX ou ODT | Carbone e Docmosis | Esses dois atendem e nós não geramos nenhum dos dois |
| Volume alto, previsível e com custo fixo | Licença perpétua do Docmosis | Sai na frente de qualquer assinatura |
| Algumas centenas de PDFs simples por mês, sem exigência de rastreabilidade | PDFMonkey a € 15 por 3 mil documentos | Custa menos esforço do que qualquer coisa que você construa |
O Document Template ganha quando o documento é jurídico, recorrente, multi-cliente e precisa ser reproduzível anos depois — não quando o problema é converter HTML em PDF bonito.
Uma nota para quem for acoplar o renderizador de PDF
Uma nota para quem for acoplar o renderizador de PDF: se o documento é gerado no servidor a partir de dados, como é o caso de contrato e CCB, o WeasyPrint é a escolha mais defensável entre as opções livres. Ele implementa CSS Paged Media a sério — @page, margin boxes, elementos correntes de cabeçalho e rodapé, referências cruzadas — enquanto o Chrome headless só oferece headerTemplate e footerTemplate em contexto isolado.
O WeasyPrint não executa JavaScript, o que é irrelevante para contrato e decisivo para relatório com gráfico interativo. Em contrapartida, o Chrome consome de 300 a 500 MB por instância, vaza memória e exige pool com reciclagem; o WeasyPrint é um processo Python que sobe em milissegundos (documentação do WeasyPrint e opções do Puppeteer, consulta em 2026-08-16).
| Critério do acoplamento | WeasyPrint | Chrome headless via Puppeteer |
|---|---|---|
| CSS Paged Media | Completo — @page, margin boxes, elementos correntes, referências cruzadas | Só headerTemplate e footerTemplate, em contexto isolado |
| Executa JavaScript | Não | Sim |
| Consumo de memória | Processo Python que sobe em milissegundos | De 300 a 500 MB por instância, com vazamento e pool a reciclar |
Modelo de cobrança e ROI
negócioUnidade de cobrança
Precificação em definição. O Document Template não tem preço fechado. Ele é uma peça da esteira de documentos e a intenção é que acompanhe a contratação do conjunto — geração, assinatura e leitura — em vez de ser vendido isolado. Não há valor a divulgar e este documento não estima nenhum.
O que dispara custo
O que dispara custo.
| Driver | Por quê |
|---|---|
| Documentos renderizados por mês | Cada renderização compila o template, valida as variáveis e grava uma linha com o HTML produzido |
| Proporção de saída em PDF | PDF envolve uma chamada de rede a um renderizador com até 60 segundos de espera, e é a operação mais cara do fluxo |
| Número de templates ativos e versões | Cada publicação guarda uma cópia integral do conteúdo; templates grandes com muitas versões crescem o armazenamento |
| Tamanho do documento gerado | O HTML de saída é gravado integralmente em rendered_documents |
Comparação de custo num cenário nomeado
Comparação de custo — cenário nomeado: financeira que emite 3.000 documentos por mês (CCB, proposta e recibo somados), sendo 2.000 deles em PDF.
| Catalisa Document Template | DocRaptor | Carbone (nuvem) | Docmosis (perpétua) | PDFMonkey | |
|---|---|---|---|---|---|
| Base de cálculo | Precificação em definição | Faixa de 5.000 docs: US$ 149/mês | Plano com cota mensal: a partir de US$ 29/mês | Licença única T-400: US$ 6.695 | Plano Pro: € 15/mês por 3.000 docs |
| Versionamento e histórico | Incluso | Você implementa | Você implementa | Você implementa | Você implementa |
| Isolamento por cliente | Incluso | Você implementa | Você implementa | Você implementa | Você implementa |
| Dado do contrato sai da sua rede | Não | Sim | Sim | Não | Sim |
Preços dos fornecedores consultados em 2026-08-16 nas páginas citadas na §5. As faixas mudam por região e negociação; a linha "você implementa" é o que separa preço de custo total, e é onde a comparação de licença engana.
ROI — a conta de guardanapo
ROI. A conta de guardanapo tem duas linhas, e nenhuma delas é licença de conversor.
flowchart LR R["ROI do Document Template"] --> L1["Linha 1 · ciclo de mudança de texto jurídico"] R --> L2["Linha 2 · custo de não conseguir reproduzir"] L1 --> A1["Antes · análise, desenvolvimento, revisão e janela de deploy"] L1 --> D1["Depois · uma chamada de API e uma publicação"] L2 --> A2["Zero até o dia em que não é"] L2 --> D2["Nesse dia, apresentar o documento exato daquela data"]
Linha 1 — o ciclo de mudança de texto jurídico. A primeira é o ciclo de mudança de texto jurídico. Com o template dentro do código, cada alteração pedida pelo jurídico consome análise, desenvolvimento, revisão e janela de deploy. Com o template como recurso, consome uma chamada de API e uma publicação. Numa operação de crédito, que revisa contrato várias vezes por ano por mudança regulatória, a diferença é medida em semanas de engenharia por ano.
Linha 2 — o custo de não conseguir reproduzir. A segunda é o custo de não conseguir reproduzir. Ele é zero até o dia em que não é — e nesse dia é uma disputa judicial ou uma resposta a fiscalização em que a sua defesa depende de conseguir apresentar exatamente o documento emitido, com a redação vigente naquela data. Esse item não entra em planilha de economia, mas é o que sustenta a decisão de compra em operação regulada.
Arquitetura
As camadas e o caminho da requisição
flowchart TD
HTTP["HTTP · Bearer JWT emitido pelo IAM"]
subgraph APP["Hono app · basePath /document-templates · applyCommonMiddleware"]
TR["templateRouter em /api/v1/document-templates · 8 rotas"]
RR["renderRouter em /api/v1/document-templates · 3 rotas"]
HL["/health · sonda pública"]
end
HTTP --> APP
APP --> PIPE["Zod parse → ResultAsync de T e AppError → handleResult"]
subgraph SVC["services/ e utils/"]
TS["TemplateService · criar, atualizar, publicar, excluir"]
RS["RenderService · renderizar em HTML ou PDF, pré-visualizar"]
PC["PDFClient · cliente HTTP do renderizador, opcional"]
VP["utils/variable-parser · validação de tipo e obrigatoriedade"]
end
PIPE --> SVC
subgraph REPO["repositories/ · 3, Prisma"]
DB[("PostgreSQL · schema doctemplates")]
T1["document_templates"]
T2["document_template_versions"]
T3["rendered_documents"]
DB --- T1
DB --- T2
DB --- T3
end
SVC --> REPO
SVC --> HB["Handlebars compila o conteúdo · escapa HTML por padrão"]
HB -->|"só quando PDF"| PDFR["pdf-renderer acompanhante · POST /api/v1/render-pdf · timeout de 60s → base64"]O elo entre as duas primeiras camadas, em notação de código, é Zod parse → ResultAsync<T, AppError> → handleResult. A compilação do conteúdo é Handlebars.compile(conteúdo), que escapa HTML por padrão; na renderização, Handlebars.compile(versao.conteudo)(dados).
Caminho de uma renderização em HTML
Caminho de uma renderização em HTML
flowchart TD
A["POST /:templateId/render"] --> B{"Template existe e pertence à organização do token?"}
B -->|Não| B404["404"]
B -->|Sim| C{"status igual a PUBLISHED?"}
C -->|Não| C400["400 · Template must be published"]
C -->|Sim| D["Busca a ÚLTIMA versão publicada, nunca o rascunho de trabalho"]
D --> E["Aplica os valores padrão declarados nas variáveis da VERSÃO"]
E --> F{"Obrigatoriedade e tipo conferem?"}
F -->|Falha| F400["400 · VALIDATION"]
F -->|Ok| G["Handlebars aplica os dados ao conteúdo da versão"]
G --> H["Grava rendered_documents · entrada, HTML, versão e businessId"]
H --> I["Publica document-templates.document.rendered → 200"]O fluxo completo: template, dados e documento
O caminho real de ponta a ponta soma três atores que a lista de camadas não mostra junta: quem escreve o template, quem manda os dados e quem guarda o resultado. O diagrama abaixo mostra a persistência do RenderedDocument e o ponto em que o fileStorageId deixa de ser preenchido — a integração com o file-storage não existe hoje (§15), e é a aplicação chamadora que guarda o binário.
sequenceDiagram
autonumber
participant Aut as Autor do template
participant DT as Document Template
participant PG as PostgreSQL doctemplates
participant App as Aplicação chamadora
participant PR as pdf-renderer acompanhante
participant FS as File Storage
Aut->>DT: "POST de template e PATCH do rascunho"
DT->>PG: "Grava document_templates em DRAFT"
Aut->>DT: "POST de publish com changelog"
DT->>PG: "Grava document_template_versions · versão imutável"
App->>DT: "POST de render com variáveis e businessId"
DT->>PG: "Lê a última versão publicada"
DT->>DT: "Valida variáveis e compila o Handlebars"
alt Saída em HTML
DT->>PG: "Grava rendered_documents com inputData e outputHtml"
DT-->>App: "200 com o HTML e o templateVersionNumber"
else Saída em PDF
DT->>PR: "POST /api/v1/render-pdf com o HTML · até 60s"
PR-->>DT: "Binário do PDF"
DT->>PG: "Grava rendered_documents · fileStorageId fica nulo"
DT-->>App: "200 com pdfBase64 e o templateVersionNumber"
App->>FS: "Upload do PDF, hoje por conta da aplicação"
endDecisões não óbvias
Decisões não óbvias.
- Renderizar usa a versão publicada; pré-visualizar usa o rascunho.
POST /:id/renderlêdocument_template_versions;POST /:id/previewlê o conteúdo corrente do template. É proposital: a pré-visualização existe para quem está escrevendo o template e precisa ver o efeito antes de publicar. Consequência prática — a pré-visualização não valida variável obrigatória, justamente para permitir ver um documento pela metade durante a edição. Não usepreviewcomo ensaio de produção. - Editar um template publicado é permitido. A edição altera o rascunho de trabalho, não a versão publicada, e não afeta nenhuma renderização até que alguém chame
POST /publish. O trade-off é que oGETdo template mostra o rascunho, não o que está sendo emitido — para ver o que está no ar, consulte a versão. - Handlebars escapa HTML por padrão.
{{variavel}}escapa o conteúdo. Um valor com<ou&sai como entidade, o que é o comportamento correto quando o dado vem do cliente. Para injetar HTML deliberadamente — uma tabela de parcelas montada fora, por exemplo — use{{{variavel}}}, sabendo que a responsabilidade pelo conteúdo passa a ser sua. - O PDF é delegado, não embutido. Não há Chrome dentro do serviço. O
PDFClientchama um renderizador por HTTP, com 60 segundos de limite, e recebe o PDF em base64. A razão é operacional: um navegador headless no mesmo processo transforma o consumo de memória do serviço em algo imprevisível, e a renderização de documento é justamente o pico. O custo dessa escolha é que semPDF_RENDERER_URLconfigurada, a rota de PDF devolve400— e hoje ela não está configurada em nenhum ambiente publicado (§15). - A versão guarda conteúdo e variáveis juntos. Poderia guardar só o conteúdo e apontar para as variáveis do template. Não guarda, de propósito: se as variáveis mudarem depois, reproduzir um documento antigo com a declaração nova produziria um resultado diferente do original. A versão precisa ser autossuficiente para ser prova.
- O documento renderizado é gravado integralmente.
rendered_documentsguardainputDataeoutputHtml. Isso é o que permite reproduzir e auditar — e é também o motivo de a §14 tratar essa tabela como repositório de dado pessoal.
Monolito vs. standalone
Monolito vs. standalone. O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/document-templates. O main.ts sobe o mesmo app com Bun.serve na porta 3019 quando DEPLOYMENT_MODE=standalone, que é o modo usado em staging e produção. Não há diferença de comportamento entre os modos: o building block não chama nenhum outro building block. O que muda é que, em standalone, 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
Glossário
| Termo | Significa |
|---|---|
| Template | O documento com marcadores no lugar dos dados variáveis. Tem slug único dentro da organização e conteúdo em HTML com sintaxe Handlebars. |
| Rascunho de trabalho | O conteúdo corrente do template, em document_templates.content. É o que a pré-visualização mostra e o que a publicação copia. Não é usado em renderização. |
| Versão | Cópia imutável do conteúdo e da declaração de variáveis, criada por POST /publish. Numerada a partir de 1. É o que a renderização usa. |
| Variável | Campo declarado com name, type, required, default e description. O nome precisa casar ^[a-zA-Z_][a-zA-Z0-9_]*$ e não pode repetir dentro do template. |
Dados de exemplo (sampleData) | Conjunto de valores usado pela pré-visualização quando nenhum valor é informado. Serve para o template ter uma aparência realista durante a edição. |
| Documento renderizado | Linha em rendered_documents com os valores de entrada, o HTML produzido, o número da versão usada e o vínculo com a operação. |
businessId | Identificador livre da operação de origem — proposta, contrato, pedido. É como você reencontra tudo que foi emitido para um caso. |
| CCB | Cédula de Crédito Bancário, título de crédito regido pelos arts. 26 a 45 da Lei nº 10.931/2004. É título executivo extrajudicial (art. 28) e tem seis requisitos essenciais de forma (art. 29). Motivou o desenho deste building block e acompanha o seed do projeto como exemplo. |
| Requisitos essenciais da CCB | Os seis incisos do art. 29: a denominação "Cédula de Crédito Bancário"; a promessa de pagar; data e lugar do pagamento, com valores das prestações ou critérios; o nome da instituição credora; data e lugar da emissão; e a assinatura do emitente e do garantidor. O § 5º, incluído pela Lei nº 13.986/2020, admite assinatura eletrônica desde que garantida a identificação inequívoca do signatário. Este building block não valida esses requisitos — ver §15. |
pdf-renderer | Serviço acompanhante que recebe HTML e devolve PDF. Não faz parte deste building block e não está publicado — ver §15. |
Modelo de dados
Modelo de dados — schema doctemplates no PostgreSQL. 3 modelos.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
DocumentTemplate | document_templates | O template e seu rascunho de trabalho | slug (único por organização), category, status, content, variables, sampleData, currentVersionNumber, isActive, deletedAt |
DocumentTemplateVersion | document_template_versions | Versão publicada, imutável | Único (templateId, versionNumber), content, variables, changelog, publishedAt |
RenderedDocument | rendered_documents | Registro de cada emissão | templateVersionNumber, inputData, outputFormat, outputHtml, fileStorageId, businessId, metadata |
erDiagram
Organization ||--o{ DocumentTemplate : possui
Organization ||--o{ RenderedDocument : possui
DocumentTemplate ||--o{ DocumentTemplateVersion : versiona
DocumentTemplate ||--o{ RenderedDocument : origina
Organization {
uuid id PK
}
DocumentTemplate {
uuid id PK
uuid organizationId FK
string slug
enum category
enum status
text content
jsonb variables
jsonb sampleData
int currentVersionNumber
bool isActive
datetime deletedAt
}
DocumentTemplateVersion {
uuid id PK
uuid templateId FK
int versionNumber
text content
jsonb variables
text changelog
datetime publishedAt
}
RenderedDocument {
uuid id PK
uuid organizationId FK
uuid templateId FK
int templateVersionNumber
jsonb inputData
enum outputFormat
text outputHtml
uuid fileStorageId
string businessId
jsonb metadata
}Templates e documentos renderizados são isolados por organizationId, com chave estrangeira para a organização do IAM. Versões herdam o isolamento pelo template, e a exclusão do template remove as versões em cascata.
Enumerações
Enumerações
| Enum | Valores |
|---|---|
DocumentTemplateStatus | DRAFT · PUBLISHED · ARCHIVED (este último declarado, nunca atribuído pelo código — ver §15) |
DocumentTemplateCategory | CONTRACT · PROPOSAL · AGREEMENT · RECEIPT · REPORT · OTHER (padrão) |
RenderOutputFormat | HTML · PDF |
| Tipos de variável | STRING · NUMBER · BOOLEAN · DATE · ARRAY · OBJECT |
Ciclo de vida do template
Ciclo de vida do template
stateDiagram-v2 [*] --> DRAFT : POST de criação do template DRAFT --> PUBLISHED : POST de publish · copia conteúdo e variáveis para uma versão imutável PUBLISHED --> PUBLISHED : POST de publish de novo · currentVersionNumber vira 2, 3, ... PUBLISHED --> PUBLISHED : PATCH altera o rascunho, NÃO a versão publicada DRAFT --> DRAFT : PATCH altera o rascunho DRAFT --> Excluido : DELETE · exclusão lógica em deletedAt PUBLISHED --> Excluido : DELETE · exclusão lógica em deletedAt Excluido --> [*] note right of DRAFT : currentVersionNumber é 0 e não renderiza — o render devolve 400 note right of PUBLISHED : renderiza sempre usando a ÚLTIMA versão; as anteriores continuam intactas note right of Excluido : some das consultas; as versões e os documentos já emitidos continuam no banco
Atenção. ARCHIVED existe no enum e nunca é atribuído pelo código (§15).
Como um valor chega ao documento
Como um valor chega ao documento
flowchart TD
A["Variáveis enviadas na requisição"] --> B["Merge com os valores padrão declarados NA VERSÃO publicada"]
B --> C{"Toda variável obrigatória está presente?"}
C -->|Não| C400["400"]
C -->|Sim| D{"Tipo confere campo a campo?"}
D -->|Falha| D400["400"]
D -->|Ok| E["Handlebars aplica os valores ao conteúdo DA VERSÃO"]
E --> F["HTML final"]
F --> G["Gravado em rendered_documents"]
G --> H["Devolvido ao chamador"]Atenção. A regra: a versão publicada é a autoridade sobre conteúdo e sobre quais variáveis existem. O rascunho não participa da renderização.
Referência da API
Prefixo HTTP: /document-templates. Em monolito, a base é http://localhost:3000. Em standalone, a porta é 3019; em staging, https://document-template.bb.stg.catalisa.app.
Atenção ao prefixo. O caminho real repete o nome do módulo: o basePath do app é /document-templates e os routers são montados em /api/v1/document-templates. A rota completa é, portanto, /document-templates/api/v1/document-templates/.... Não é erro de digitação — é o caminho que o serviço responde.
Todas as 11 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.
flowchart LR B["basePath /document-templates"] --> M["Montagem em /api/v1/document-templates"] M --> T["templateRouter · 8 rotas de catálogo, versão e publicação"] M --> R["renderRouter · render, render/pdf e preview"] B --> H["/health · pública, fora da contagem das 11"]
Templates — /document-templates/api/v1/document-templates
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /document-templates/api/v1/document-templates | Cria template em DRAFT. Responde 201 | DOCUMENT_TEMPLATES_CREATE |
GET | /document-templates/api/v1/document-templates | Lista templates, paginado | DOCUMENT_TEMPLATES_READ |
GET | /document-templates/api/v1/document-templates/:templateId | Busca o template (mostra o rascunho) | DOCUMENT_TEMPLATES_READ |
PATCH | /document-templates/api/v1/document-templates/:templateId | Atualiza o rascunho | DOCUMENT_TEMPLATES_UPDATE |
DELETE | /document-templates/api/v1/document-templates/:templateId | Exclusão lógica. Responde 204 | DOCUMENT_TEMPLATES_DELETE |
POST | /document-templates/api/v1/document-templates/:templateId/publish | Congela o rascunho em uma versão nova | DOCUMENT_TEMPLATES_PUBLISH |
GET | /document-templates/api/v1/document-templates/:templateId/versions | Lista versões, paginado | DOCUMENT_TEMPLATES_READ |
GET | /document-templates/api/v1/document-templates/:templateId/versions/:versionNumber | Busca uma versão específica | DOCUMENT_TEMPLATES_READ |
Filtros aceitos em GET /document-templates: filter[status], filter[category], filter[active] (true/false). Paginação por page[number] e page[size].
Renderização — mesmo prefixo
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /document-templates/api/v1/document-templates/:templateId/render | Renderiza em HTML e registra a emissão | DOCUMENT_TEMPLATES_RENDER |
POST | /document-templates/api/v1/document-templates/:templateId/render/pdf | Renderiza em PDF (base64) e registra a emissão | DOCUMENT_TEMPLATES_RENDER |
POST | /document-templates/api/v1/document-templates/:templateId/preview | Pré-visualiza o rascunho, sem validar e sem gravar | DOCUMENT_TEMPLATES_RENDER |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /document-templates/health | Sonda de disponibilidade. Pública, não contabilizada nas 11 rotas |
POST /document-templates/api/v1/document-templates
Cria o template em DRAFT. O corpo usa o envelope JSON:API — data.attributes é obrigatório nesta rota.
Request
{
"data": {
"type": "document-templates",
"attributes": {
"name": "CCB - Cédula de Crédito Bancário",
"slug": "ccb-credito-pessoal",
"description": "Cédula para operações de crédito pessoal",
"category": "CONTRACT",
"content": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1><p>Devedor: {{nome_cliente}} — CPF {{cpf_cliente}}</p><p>Valor: R$ {{valor_principal}}</p>",
"variables": [
{ "name": "nome_cliente", "type": "STRING", "required": true, "description": "Nome completo do devedor" },
{ "name": "cpf_cliente", "type": "STRING", "required": true },
{ "name": "valor_principal", "type": "NUMBER", "required": true }
],
"sampleData": { "nome_cliente": "Fulano de Tal", "cpf_cliente": "000.000.000-00", "valor_principal": 10000 }
}
}
}{
"data": {
"type": "document-templates",
"attributes": {
"name": "CCB - Cédula de Crédito Bancário",
"slug": "ccb-credito-pessoal",
"description": "Cédula para operações de crédito pessoal",
"category": "CONTRACT",
"content": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1><p>Devedor: {{nome_cliente}} — CPF {{cpf_cliente}}</p><p>Valor: R$ {{valor_principal}}</p>",
"variables": [
{ "name": "nome_cliente", "type": "STRING", "required": true, "description": "Nome completo do devedor" },
{ "name": "cpf_cliente", "type": "STRING", "required": true },
{ "name": "valor_principal", "type": "NUMBER", "required": true }
],
"sampleData": { "nome_cliente": "Fulano de Tal", "cpf_cliente": "000.000.000-00", "valor_principal": 10000 }
}
}
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–200) | Sim | Nome de exibição |
slug | string (1–100) | Sim | Identificador legível. Somente [a-z0-9-]. Único por organização |
description | string (máx. 5000) | Não | — |
category | enum | Não | Padrão OTHER |
content | string | Sim | HTML com marcadores Handlebars |
variables | VariableDefinition[] | Sim | Pode ser lista vazia. Nomes únicos e no formato ^[a-zA-Z_][a-zA-Z0-9_]*$ |
sampleData | objeto | Não | Valores usados pela pré-visualização |
metadata | objeto | Não | Campo livre |
Resposta 201 — o template nasce com status: "DRAFT" e currentVersionNumber: 0.
Erros
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod; slug fora do formato; nome de variável inválido ou repetido |
403 | Sem DOCUMENT_TEMPLATES_CREATE, ou token sem organizationId |
409 | Já existe template com esse slug na organização |
POST /document-templates/api/v1/document-templates/:templateId/publish
Congela o rascunho corrente em uma versão nova e coloca o template em PUBLISHED.
Request (corpo opcional)
{ "changelog": "Inclui cláusula de portabilidade de crédito" }{ "changelog": "Inclui cláusula de portabilidade de crédito" }O changelog é livre, até 1000 caracteres, e é a única descrição que vai existir da mudança. Preencha — é ele que responde "o que mudou da versão 2 para a 3" numa auditoria.
Resposta 200 — o template volta com status: "PUBLISHED" e currentVersionNumber incrementado.
Publicar de novo sem alterar nada cria uma versão idêntica com número novo. Não há verificação de mudança.
POST /document-templates/api/v1/document-templates/:templateId/render
Renderiza em HTML usando a última versão publicada e grava a emissão.
Request
{
"variables": {
"nome_cliente": "Maria Souza",
"cpf_cliente": "123.456.789-00",
"valor_principal": 15000
},
"businessId": "proposta-2026-000481",
"metadata": { "origem": "esteira-consignado" }
}{
"variables": {
"nome_cliente": "Maria Souza",
"cpf_cliente": "123.456.789-00",
"valor_principal": 15000
},
"businessId": "proposta-2026-000481",
"metadata": { "origem": "esteira-consignado" }
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
variables | objeto | Sim | Valores das variáveis. Chaves precisam bater com os nomes declarados na versão |
outputFormat | "HTML" | Não | Nesta rota só aceita HTML. Para PDF, use a rota /render/pdf |
businessId | string | Não | Identificador da operação de origem |
metadata | objeto | Não | Campo livre, gravado junto com a emissão |
Resposta 200
{
"data": {
"type": "rendered-documents",
"id": "7c1f9b2e-0000-0000-0000-000000000000",
"attributes": {
"html": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>...",
"templateId": "3a2b1c00-0000-0000-0000-000000000000",
"templateVersionNumber": 3
}
}
}{
"data": {
"type": "rendered-documents",
"id": "7c1f9b2e-0000-0000-0000-000000000000",
"attributes": {
"html": "<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>...",
"templateId": "3a2b1c00-0000-0000-0000-000000000000",
"templateVersionNumber": 3
}
}
}O templateVersionNumber da resposta é o que você guarda na sua operação. É ele que responde, depois, qual redação foi usada.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Template ainda em DRAFT — Template must be published before rendering |
400 | VALIDATION | Variável obrigatória ausente — Required variable 'X' is missing |
400 | VALIDATION | Tipo errado — Variable 'X' must be a number |
400 | VALIDATION | Falha na compilação do Handlebars — Template rendering failed: ... |
404 | NOT_FOUND | Template inexistente, excluído, ou de outra organização |
404 | NOT_FOUND | Template marcado como publicado mas sem nenhuma versão gravada |
POST /document-templates/api/v1/document-templates/:templateId/render/pdf
Mesmo contrato de entrada da rota de HTML. Renderiza o HTML, envia ao serviço acompanhante e devolve o PDF em base64 no campo pdfBase64.
Resposta 200
{
"data": {
"type": "rendered-documents",
"id": "9d4e2f11-0000-0000-0000-000000000000",
"attributes": {
"templateId": "3a2b1c00-0000-0000-0000-000000000000",
"templateVersionNumber": 3,
"outputFormat": "PDF",
"pdfBase64": "JVBERi0xLjQKJcfsj6IK..."
}
}
}{
"data": {
"type": "rendered-documents",
"id": "9d4e2f11-0000-0000-0000-000000000000",
"attributes": {
"templateId": "3a2b1c00-0000-0000-0000-000000000000",
"templateVersionNumber": 3,
"outputFormat": "PDF",
"pdfBase64": "JVBERi0xLjQKJcfsj6IK..."
}
}
}Erros adicionais
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | PDF rendering is not configured. Set PDF_RENDERER_URL environment variable. — é a resposta atual em staging e produção (§15) |
500 | INTERNAL | PDF renderer service is unavailable — a variável está configurada mas o serviço não responde |
500 | INTERNAL | O renderizador respondeu com erro ou estourou os 60 segundos |
O PDF não é gravado: apenas o HTML de origem vai para rendered_documents. O campo fileStorageId existe na tabela e nunca é preenchido (§15) — guardar o binário é responsabilidade de quem chama.
POST /document-templates/api/v1/document-templates/:templateId/preview
Pré-visualiza o rascunho de trabalho. Não exige template publicado, não valida variáveis e não grava nada.
Request (corpo opcional)
{ "variables": { "nome_cliente": "Fulano de Tal" } }{ "variables": { "nome_cliente": "Fulano de Tal" } }Sem variables, usa o sampleData do template. Sem sampleData, renderiza com os valores padrão declarados e deixa o resto em branco.
Resposta 200
{ "data": { "type": "template-previews", "attributes": { "html": "<h1>...</h1>" } } }{ "data": { "type": "template-previews", "attributes": { "html": "<h1>...</h1>" } } }Use durante a escrita do template. Não use como ensaio de emissão: a pré-visualização passa em casos que a renderização recusa.
Início rápido
Do zero a um contrato emitido, com validação funcionando. Comandos escritos para staging; não executados nesta redação.
flowchart LR P1["1 · autenticar no IAM"] --> P2["2 · criar o template em DRAFT"] P2 --> P3["3 · tentar renderizar e ver a recusa"] P3 --> P4["4 · publicar"] P4 --> P5["5 · renderizar de verdade"] P5 --> P6["6 · omitir uma variável e ver a validação proteger"]
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)
BASE=https://document-template.bb.stg.catalisa.app/document-templates/api/v1/document-templates
echo "${TOKEN:0:12}…"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)
BASE=https://document-template.bb.stg.catalisa.app/document-templates/api/v1/document-templates
echo "${TOKEN:0:12}…"Resposta esperada: os primeiros caracteres do JWT. Se sair vazio, a autenticação falhou e nada adiante vai funcionar.
eyJhbGciOiJI…eyJhbGciOiJI…2. Criar o template
TPL=$(curl -s -X POST "$BASE" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"data": {
"type": "document-templates",
"attributes": {
"name": "Recibo de Liberação",
"slug": "recibo-liberacao-teste",
"category": "RECEIPT",
"content": "<h1>Recibo</h1><p>Recebi de {{instituicao}} a quantia de R$ {{valor}}.</p><p>{{nome_cliente}} — CPF {{cpf_cliente}}</p>",
"variables": [
{ "name": "instituicao", "type": "STRING", "required": true },
{ "name": "valor", "type": "NUMBER", "required": true },
{ "name": "nome_cliente", "type": "STRING", "required": true },
{ "name": "cpf_cliente", "type": "STRING", "required": true }
]
}
}
}')
TPL_ID=$(echo "$TPL" | jq -r '.data.id')
echo "$TPL" | jq '.data.attributes | {status, currentVersionNumber}'TPL=$(curl -s -X POST "$BASE" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"data": {
"type": "document-templates",
"attributes": {
"name": "Recibo de Liberação",
"slug": "recibo-liberacao-teste",
"category": "RECEIPT",
"content": "<h1>Recibo</h1><p>Recebi de {{instituicao}} a quantia de R$ {{valor}}.</p><p>{{nome_cliente}} — CPF {{cpf_cliente}}</p>",
"variables": [
{ "name": "instituicao", "type": "STRING", "required": true },
{ "name": "valor", "type": "NUMBER", "required": true },
{ "name": "nome_cliente", "type": "STRING", "required": true },
{ "name": "cpf_cliente", "type": "STRING", "required": true }
]
}
}
}')
TPL_ID=$(echo "$TPL" | jq -r '.data.id')
echo "$TPL" | jq '.data.attributes | {status, currentVersionNumber}'Resposta esperada: {"status": "DRAFT", "currentVersionNumber": 0}.
3. Tentar renderizar antes de publicar — e ver a recusa
curl -s -X POST "$BASE/$TPL_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables":{"instituicao":"Financeira Exemplo","valor":15000,"nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jqcurl -s -X POST "$BASE/$TPL_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables":{"instituicao":"Financeira Exemplo","valor":15000,"nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jq{ "error": "VALIDATION", "message": "Template must be published before rendering" }{ "error": "VALIDATION", "message": "Template must be published before rendering" }4. Publicar
curl -s -X POST "$BASE/$TPL_ID/publish" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"changelog":"Versão inicial"}' | jq '.data.attributes | {status, currentVersionNumber}'curl -s -X POST "$BASE/$TPL_ID/publish" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"changelog":"Versão inicial"}' | jq '.data.attributes | {status, currentVersionNumber}'Resposta esperada: {"status": "PUBLISHED", "currentVersionNumber": 1}.
5. Renderizar de verdade
curl -s -X POST "$BASE/$TPL_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"variables": {
"instituicao": "Financeira Exemplo",
"valor": 15000,
"nome_cliente": "Maria Souza",
"cpf_cliente": "123.456.789-00"
},
"businessId": "proposta-teste-001"
}' | jq '.data.attributes | {templateVersionNumber, html}'curl -s -X POST "$BASE/$TPL_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"variables": {
"instituicao": "Financeira Exemplo",
"valor": 15000,
"nome_cliente": "Maria Souza",
"cpf_cliente": "123.456.789-00"
},
"businessId": "proposta-teste-001"
}' | jq '.data.attributes | {templateVersionNumber, html}'Resposta esperada: a versão usada e o HTML já com os valores aplicados.
{
"templateVersionNumber": 1,
"html": "<h1>Recibo</h1><p>Recebi de Financeira Exemplo a quantia de R$ 15000.</p><p>Maria Souza — CPF 123.456.789-00</p>"
}{
"templateVersionNumber": 1,
"html": "<h1>Recibo</h1><p>Recebi de Financeira Exemplo a quantia de R$ 15000.</p><p>Maria Souza — CPF 123.456.789-00</p>"
}6. Confirmar que a validação protege o documento
curl -s -X POST "$BASE/$TPL_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables":{"instituicao":"Financeira Exemplo","nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jqcurl -s -X POST "$BASE/$TPL_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables":{"instituicao":"Financeira Exemplo","nome_cliente":"Maria Souza","cpf_cliente":"123.456.789-00"}}' | jq{ "error": "VALIDATION", "message": "Required variable 'valor' is missing" }{ "error": "VALIDATION", "message": "Required variable 'valor' is missing" }Nenhum documento foi gerado e nenhuma linha foi gravada. É esse comportamento que impede contrato com campo em branco de chegar ao cliente.
Credenciais de staging, conforme AMBIENTES.md. Nunca use credencial de produção em documentação ou script de exemplo.
Receitas
Emitir uma CCB a partir do resultado da esteira de crédito
O caso central. O calculations-engine produz os números; o Document Template produz o documento.
flowchart LR CE["Calculations Engine · parcela, IOF, CET, cronograma"] --> V["Variáveis da CCB"] CU["Customers · nome, CPF"] --> V V --> R["POST de render no template da CCB"] R --> D["CCB emitida, com templateVersionNumber e id"]
1. Enviar os valores da esteira e emitir a cédula
# 1. Os valores vêm da sua esteira — aqui, ilustrados
curl -s -X POST "$BASE/$CCB_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"variables": {
"instituicao": "Financeira Exemplo S.A.",
"cnpj_instituicao": "00.000.000/0001-00",
"nome_cliente": "Maria Souza",
"cpf_cliente": "123.456.789-00",
"valor_principal": 15000,
"taxa_mensal": 2.19,
"taxa_anual": 29.68,
"cet_anual": 34.12,
"num_parcelas": 36,
"valor_parcela": 612.44,
"data_emissao": "2026-08-16",
"data_vencimento_primeira": "2026-09-16",
"sistema_amortizacao": "PRICE",
"garantias": "Sem garantia real",
"iof_total": 412.55
},
"businessId": "proposta-2026-000481"
}' | jq '{versao: .data.attributes.templateVersionNumber, id: .data.id}'# 1. Os valores vêm da sua esteira — aqui, ilustrados
curl -s -X POST "$BASE/$CCB_ID/render" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"variables": {
"instituicao": "Financeira Exemplo S.A.",
"cnpj_instituicao": "00.000.000/0001-00",
"nome_cliente": "Maria Souza",
"cpf_cliente": "123.456.789-00",
"valor_principal": 15000,
"taxa_mensal": 2.19,
"taxa_anual": 29.68,
"cet_anual": 34.12,
"num_parcelas": 36,
"valor_parcela": 612.44,
"data_emissao": "2026-08-16",
"data_vencimento_primeira": "2026-09-16",
"sistema_amortizacao": "PRICE",
"garantias": "Sem garantia real",
"iof_total": 412.55
},
"businessId": "proposta-2026-000481"
}' | jq '{versao: .data.attributes.templateVersionNumber, id: .data.id}'Resposta esperada: a versão que gerou o documento e o identificador da emissão.
{ "versao": 3, "id": "7c1f9b2e-0000-0000-0000-000000000000" }{ "versao": 3, "id": "7c1f9b2e-0000-0000-0000-000000000000" }Armadilhas.
- Guarde o
templateVersionNumbere oiddo documento renderizado na sua operação, não só aqui. São eles que ligam a operação à redação usada. - Valores monetários são
NUMBER. Se a sua esteira manda"15000,00"como texto, a validação recusa comVariable 'valor_principal' must be a number. Converta antes. - Datas aceitam
Dateou texto ISO."16/08/2026"é reprovado — use"2026-08-16". - Formatação de moeda é responsabilidade do template. O Handlebars não formata número:
{{valor_principal}}imprime15000, não15.000,00. Envie o valor já formatado numa variável de texto separada se a apresentação importar. - O seed do projeto (
prisma/seed-from-services.ts) traz uma CCB completa com quinze variáveis e uma segunda versão com cláusula de portabilidade. É o melhor ponto de partida para escrever a sua.
Mudar a redação de um contrato sem afetar o que já está em uso
Alterar o texto vigente sem que nenhuma emissão em andamento mude de comportamento antes da hora.
flowchart LR S1["1 · PATCH altera o rascunho"] --> S2["2 · preview confere o resultado"] S2 --> S3["3 · publish cria a versão nova"] S1 -.->|"até o passo 3, o render usa a versão anterior"| S3
1. Alterar o rascunho — não afeta nenhuma renderização ainda
# 1. Alterar o rascunho — não afeta nenhuma renderização ainda
curl -s -X PATCH "$BASE/$TPL_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"content":"<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>... nova redação ..."}' | jq '.data.attributes.currentVersionNumber'# 1. Alterar o rascunho — não afeta nenhuma renderização ainda
curl -s -X PATCH "$BASE/$TPL_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"content":"<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>... nova redação ..."}' | jq '.data.attributes.currentVersionNumber'Resposta esperada: o número da versão não muda — o PATCH mexe no rascunho, não na versão publicada.
222. Conferir o resultado antes de publicar
# 2. Conferir o resultado antes de publicar
curl -s -X POST "$BASE/$TPL_ID/preview" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' \
| jq -r '.data.attributes.html' > /tmp/previa.html# 2. Conferir o resultado antes de publicar
curl -s -X POST "$BASE/$TPL_ID/preview" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' \
| jq -r '.data.attributes.html' > /tmp/previa.htmlResposta esperada: nenhuma saída no terminal e o arquivo gravado com o rascunho já renderizado.
$ head -c 60 /tmp/previa.html
<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>$ head -c 60 /tmp/previa.html
<h1>CÉDULA DE CRÉDITO BANCÁRIO</h1>3. Publicar quando estiver certo
# 3. Publicar quando estiver certo
curl -s -X POST "$BASE/$TPL_ID/publish" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"changelog":"Ajuste na cláusula de vencimento antecipado — parecer jurídico 2026-08"}' | jq# 3. Publicar quando estiver certo
curl -s -X POST "$BASE/$TPL_ID/publish" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"changelog":"Ajuste na cláusula de vencimento antecipado — parecer jurídico 2026-08"}' | jqResposta esperada: agora sim o número da versão sobe, e é essa versão que passa a ser usada nas próximas renderizações.
{ "data": { "type": "document-templates", "attributes": { "status": "PUBLISHED", "currentVersionNumber": 3 } } }{ "data": { "type": "document-templates", "attributes": { "status": "PUBLISHED", "currentVersionNumber": 3 } } }Armadilhas.
- Entre o passo 1 e o passo 3, as renderizações continuam usando a versão anterior. Isso é o comportamento desejado, mas surpreende quem espera que o
PATCHjá valha. GETno template mostra o rascunho, não o que está sendo emitido. Para ver o que está no ar, consulteGET /:id/versions/:ncom ocurrentVersionNumber.- Se você adicionar uma variável obrigatória nova, a publicação passa a quebrar as chamadas de renderização que ainda não enviam esse campo. Publique a mudança de contrato de dados junto com o deploy de quem chama, ou declare a variável com
default. - Não há verificação de mudança: publicar sem alterar nada cria uma versão idêntica.
Reproduzir um documento emitido meses atrás
Recuperar a redação exata que gerou um documento antigo, a partir do número da versão que você guardou na operação.
1. Buscar a versão pelo número guardado
# Você guardou o número da versão na sua operação
curl -s "$BASE/$TPL_ID/versions/3" -H "Authorization: Bearer $TOKEN" \
| jq '{versao: .data.attributes.versionNumber,
publicada_em: .data.attributes.publishedAt,
changelog: .data.attributes.changelog,
variaveis: [.data.attributes.variables[].name]}'# Você guardou o número da versão na sua operação
curl -s "$BASE/$TPL_ID/versions/3" -H "Authorization: Bearer $TOKEN" \
| jq '{versao: .data.attributes.versionNumber,
publicada_em: .data.attributes.publishedAt,
changelog: .data.attributes.changelog,
variaveis: [.data.attributes.variables[].name]}'Resposta esperada: a versão, a data de publicação, o changelog e os nomes das variáveis que existiam naquela época.
{
"versao": 3,
"publicada_em": "2026-03-11T14:22:07.512Z",
"changelog": "Inclui cláusula de portabilidade de crédito",
"variaveis": ["nome_cliente", "cpf_cliente", "valor_principal", "iof_total"]
}{
"versao": 3,
"publicada_em": "2026-03-11T14:22:07.512Z",
"changelog": "Inclui cláusula de portabilidade de crédito",
"variaveis": ["nome_cliente", "cpf_cliente", "valor_principal", "iof_total"]
}Armadilhas.
- A versão devolve o conteúdo e a declaração de variáveis daquela época. Os valores usados na emissão estão em
rendered_documents.inputData— e hoje não há endpoint que leia essa tabela (§15). Para uma auditoria, o acesso é por consulta direta ao banco. - Renderizar de novo com a versão antiga não é possível pela API: a renderização sempre usa a última versão publicada. Reproduzir exige recuperar o HTML gravado.
Montar uma tabela de parcelas dentro do documento
Handlebars é uma engine sem lógica, mas tem #each. Declare a lista como ARRAY:
<table>
<tr><th>Parcela</th><th>Vencimento</th><th>Valor</th></tr>
{{#each parcelas}}
<tr><td>{{this.numero}}</td><td>{{this.vencimento}}</td><td>R$ {{this.valor}}</td></tr>
{{/each}}
</table><table>
<tr><th>Parcela</th><th>Vencimento</th><th>Valor</th></tr>
{{#each parcelas}}
<tr><td>{{this.numero}}</td><td>{{this.vencimento}}</td><td>R$ {{this.valor}}</td></tr>
{{/each}}
</table>{ "name": "parcelas", "type": "ARRAY", "required": true }{ "name": "parcelas", "type": "ARRAY", "required": true }Armadilhas.
- A validação confere que
parcelasé um array. Não confere o que tem dentro — objeto com a chave errada some do documento em silêncio. Valide a forma dos itens antes de enviar. {{this.valor}}sai sem formatação. Formate os números na sua aplicação antes de montar o array.- O extrator de variáveis do template (
extractTemplateVariables) ignora#eache#if, mas ele só é usado como utilitário — a declaração de variáveis continua sendo manual e é ela que vale.
Distinguir "não configurado" de "fora do ar" na geração de PDF
Saber, pela resposta, se o problema é falta de infraestrutura ou falha do renderizador — porque o tratamento é diferente.
1. Chamar a rota de PDF e olhar só o par erro e mensagem
curl -s -X POST "$BASE/$TPL_ID/render/pdf" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables":{ ... }}' | jq '{error, message}'curl -s -X POST "$BASE/$TPL_ID/render/pdf" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"variables":{ ... }}' | jq '{error, message}'Resposta esperada hoje, em staging e produção:
{ "error": "VALIDATION", "message": "PDF rendering is not configured. Set PDF_RENDERER_URL environment variable." }{ "error": "VALIDATION", "message": "PDF rendering is not configured. Set PDF_RENDERER_URL environment variable." }2. Ler a resposta na tabela
| Resposta | Significa | O que fazer |
|---|---|---|
400 · PDF rendering is not configured | PDF_RENDERER_URL não está definida no ambiente | É o estado atual de staging e produção (§15). Use a saída em HTML |
500 · PDF renderer service is unavailable | A variável existe, o serviço não responde | Verifique o serviço acompanhante e a rede |
500 · PDF renderer returned 5xx | O serviço respondeu com erro | Verifique o HTML de entrada e os logs do renderizador |
Armadilha. A distinção importa: o 400 não é problema seu de integração, é ausência de infraestrutura. Não trate como erro de requisição nem tente repetir.
Integração com outros building blocks
O Document Template é a primeira peça da cadeia de documentos da Catalisa. Ele gera; o e-signature coleta a assinatura; o data-extraction lê o documento que volta preenchido ou o comprovante que o cliente envia.
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token e define o organizationId que isola templates e documentos | Sim |
| E-Signature | Recebe o documento gerado e coleta as assinaturas | Não |
| File Storage | Guarda o PDF gerado — hoje a ligação é manual (§15) | Não |
| Calculations Engine | Produz os números da CCB: parcela, IOF, CET, cronograma | Não |
| Decision Platform | Orquestra a esteira que decide, calcula e manda emitir o documento | Não |
| Customers | Fornece os dados cadastrais do devedor que preenchem o documento | Não |
| Webhooks Engine | Entrega document-templates.document.rendered a sistemas externos | Não |
| Audit Trail | Registra quem publicou versão e quem emitiu documento | Não |
A cadeia de documentos, ponta a ponta
A cadeia de documentos, ponta a ponta
flowchart TD CU["Customers"] -->|"dados cadastrais"| DT CE["Calculations Engine"] -->|"parcela, IOF, CET"| DT DP["Decision Platform"] -->|"decide e orquestra"| DT DT["Document Template · gera a CCB a partir da versão publicada"] DT -->|"HTML ou PDF"| ES["E-Signature · coleta assinatura dos signatários por e-mail"] ES -->|"documento assinado"| FS["File Storage · guarda o assinado"] ES -->|"documento assinado"| WE["Webhooks Engine · avisa o sistema do cliente"]
E na volta, quando o cliente manda um comprovante ou um documento preenchido:
flowchart LR FS2["File Storage"] -->|"arquivo"| DE["Data Extraction · lê e devolve JSON"] DE -->|"dados estruturados"| EST["De volta à esteira"]
Gerar a CCB aqui e mandar assinar no e-signature
O par mais forte da cadeia é este: o Document Template emite a cédula, o e-signature coleta as assinaturas. A ligação entre os dois é o fileId do file-storage — o SignatureDocument guarda esse campo e o serviço de assinatura baixa o arquivo por ele antes de mandar ao provedor.
O passo de upload é manual hoje: o Document Template devolve o PDF em base64 e não grava nada no file-storage, porque fileStorageId nunca é preenchido (§15). Quem orquestra a esteira faz o upload e passa o fileId adiante.
sequenceDiagram autonumber participant App as Aplicação · esteira de crédito participant DT as Document Template participant PR as pdf-renderer acompanhante participant FS as File Storage participant ES as E-Signature participant PV as Provedor de assinatura App->>DT: "POST de render/pdf no template da CCB · variáveis e businessId" DT->>PR: "HTML compilado da versão publicada" PR-->>DT: "Binário do PDF" DT-->>App: "200 · pdfBase64 e templateVersionNumber" App->>FS: "POST /file-storage/api/v1/files com o PDF" FS-->>App: "fileId" App->>ES: "POST /e-signature/api/v1/esignature/requests · fileId, signatários e businessId" ES->>FS: "Gera URL de download e baixa o arquivo do fileId" ES->>PV: "Cria o envelope com o documento e os signatários" PV-->>ES: "viewUrl e id externo" ES-->>App: "201 · id da solicitação e viewUrl" PV->>ES: "Webhook de assinado ou concluído" App->>ES: "GET /e-signature/api/v1/esignature/requests/:id/download" ES-->>App: "URL do documento assinado, com validade"
Atenção. O e-signature aceita fileId ou fileBase64 na criação da solicitação; um dos dois é obrigatório. Passar o base64 direto pula o file-storage, mas também abre mão de ter o documento original guardado com o resto da operação.
Este diagrama é o argumento comercial: gerar, assinar e ler são o mesmo problema visto de três ângulos, e na Catalisa são três peças que já compartilham o token, o tenant e o businessId. Quem monta essa cadeia com três fornecedores diferentes gasta a maior parte do esforço em amarrar identidade, isolamento e correlação entre eles — trabalho que aqui não existe porque as peças já nasceram no mesmo catálogo.
O businessId é o fio que costura tudo: o mesmo identificador de proposta vai no documento gerado, na solicitação de assinatura e na extração do comprovante. Listar tudo que aconteceu com uma operação é filtrar por um campo em três serviços.
Configuração e operação
Variáveis de ambiente
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATABASE_URL | PostgreSQL. O building block usa o schema doctemplates | Sim | — |
REDIS_URL | Redis, usado pelo limite de taxa e pelo publicador de eventos | Sim | — |
JWT_SECRET | Segredo HS256 compartilhado com o IAM. Mínimo 44 caracteres | Sim | — |
PDF_RENDERER_URL | URL do serviço acompanhante de PDF. Sem ela, a rota de PDF devolve 400 | Não | http://pdf-renderer:3020 no cliente, mas o PDFClient só é registrado se a variável existir |
PORT | Porta em standalone | Não | 3000 (a topologia expõe 3019) |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
O
PDFClientsó entra no container de dependências quandoPDF_RENDERER_URLestá definida. Se ela não estiver, o serviço sobe normalmente e apenas a rota de PDF fica indisponível — o que é o comportamento certo, mas silencioso. Verifique na subida.
Dependências de infraestrutura
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema doctemplates — templates, versões e documentos emitidos |
| Redis | Contadores de limite de taxa e publicação de eventos |
| IAM | Verificação do token; o organizationId vem do claim assinado |
pdf-renderer | Serviço acompanhante que converte HTML em PDF. Opcional e hoje não publicado (§15) |
Limites e quotas
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
| Corpo da requisição | 1 MB | applyCommonMiddleware — vale para o content do template e para o corpo de renderização |
| Nome do template | 200 caracteres | Zod |
slug | 100 caracteres, [a-z0-9-] | Zod e serviço |
| Descrição | 5.000 caracteres | Zod |
changelog | 1.000 caracteres | Zod |
| Espera pelo renderizador de PDF | 60 segundos | PDFClient |
| Limite de taxa global | 10.000 requisições por minuto por IP, quando ligado | RATE_LIMIT_GLOBAL_MAX |
Catálogo de erros
Catálogo de erros
O envelope de erro é { "error": "<código>", "message": "<texto>", "details": <opcional> }.
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado, variável faltando, tipo errado, template não publicado, ou PDF não configurado | Leia a message: ela nomeia a variável ou a condição |
401 | — | Token ausente, inválido ou expirado | Renove no IAM |
403 | — | Falta a permissão exigida, ou o token não carrega organizationId | Confira o papel e a organização no token |
404 | NOT_FOUND | Template, versão ou recurso inexistente, excluído, ou de outra organização | Confira o identificador. Note que "de outra organização" e "inexistente" são a mesma resposta, de propósito |
409 | CONFLICT | slug já usado na organização | Escolha outro |
413 | — | Corpo acima de 1 MB | Reduza o template ou o volume de variáveis |
429 | — | Limite de taxa estourado | Aplique recuo exponencial |
500 | INTERNAL | Falha no banco, ou o renderizador de PDF indisponível | Verifique a message e os logs |
Observabilidade
Observabilidade.
GET /document-templates/healthresponde a sonda de disponibilidade. É a rota usada pelohealthcheckda pilha em staging e produção, viaHEALTH_BASE_PATH=document-templates.- Cada renderização publica
document-templates.document.renderedcom o identificador do documento, o do template, o número da versão e obusinessId. É a métrica de volume mais confiável, porque só é emitida depois de a linha ser gravada. - Falhas de PDF são registradas pelo
PDFClientcom a mensagem original do renderizador antes de viraremINTERNAL. - Não há métrica de latência de renderização exposta. Para medir, use o intervalo entre a requisição e o evento.
Segurança e compliance
Isolamento entre tenants
Isolamento entre tenants. O organizationId vem do claim assinado do JWT e nunca do corpo. As três rotas de renderização e as oito de template passam pelo middleware local requireOrganization, que devolve 403 quando o claim está ausente.
flowchart TD
A["Requisição com Bearer JWT"] --> B["authMiddleware · verifica a assinatura"]
B --> C["requirePermission · confere a permissão exigida"]
C --> D{"O token carrega organizationId?"}
D -->|Não| E["403 · Organization context required"]
D -->|Sim| F["Repositório · WHERE organizationId e deletedAt nulo"]
F --> G{"Achou dentro da organização?"}
G -->|Não| H["404 · mesma resposta para inexistente e de outro tenant"]
G -->|Sim| I["Segue o fluxo"]Toda consulta de repositório inclui organizationId na cláusula WHERE — findById, findBySlug e findMany filtram por organização e por deletedAt: null. Um template de outra organização responde 404, não 403: a distinção permitiria descobrir a existência do recurso.
O que este building block guarda de dado pessoal
O que este building block guarda de dado pessoal. Esta é a parte que precisa estar clara para quem avalia LGPD.
| Onde | O que fica gravado | Por quanto tempo |
|---|---|---|
rendered_documents.inputData | Todos os valores enviados na renderização — nome, CPF, valor, condições de crédito | Indefinidamente. Não há política de retenção nem expurgo automático |
rendered_documents.outputHtml | O documento inteiro, com os dados já aplicados | Indefinidamente |
document_templates.sampleData | Valores de exemplo, que muitas vezes são copiados de um caso real | Indefinidamente |
document_template_versions.content | O texto do contrato. Não contém dado de cliente | Indefinidamente |
Em outras palavras: a tabela de documentos emitidos é um repositório de contratos com dado pessoal, e é por isso que ela existe — sem ela não há reprodutibilidade. Trate-a com o mesmo cuidado do banco de clientes: controle de acesso ao banco, cifragem em repouso no volume, e um processo definido para atender pedido de eliminação. Hoje esse expurgo é manual (§15).
Atenção. Evite colocar dado real em sampleData; ele é retornado em toda leitura do template, para quem tiver apenas DOCUMENT_TEMPLATES_READ.
O conteúdo não sai da sua infraestrutura
O conteúdo não sai da sua infraestrutura. A compilação do Handlebars acontece dentro do processo. A única saída é a chamada ao serviço acompanhante de PDF, e mesmo essa é uma URL interna que você controla. Não há chamada a serviço de terceiro em nenhum caminho — o que é a diferença material frente a um conversor de PDF em nuvem, onde o contrato com CPF e condições de crédito trafega para fora.
Escapamento de conteúdo
Escapamento de conteúdo. O Handlebars escapa HTML em {{variavel}} por padrão. Um valor vindo do cliente com <script> sai como texto, não como marcação. A forma {{{variavel}}} desliga o escapamento e deve ser reservada para conteúdo que a sua aplicação montou e conferiu.
O template é código que você executa
O template é código que você executa. Quem tem DOCUMENT_TEMPLATES_CREATE ou _UPDATE escreve HTML que será compilado pelo serviço e, na rota de PDF, carregado por um renderizador. Trate essas duas permissões como privilégio elevado e conceda-as a poucas contas — o mesmo critério que você usaria para quem pode alterar uma regra de negócio em produção.
Permissões exigidas
Permissões exigidas. Todas as rotas exigem token válido do IAM mais a permissão específica. As seis permissões (CREATE, READ, UPDATE, DELETE, PUBLISH, RENDER) permitem separar quem escreve o template de quem publica e de quem emite — a separação que uma operação regulada normalmente exige entre jurídico, aprovador e esteira. Note que, hoje, apenas o papel ADMIN recebe essas permissões no catálogo padrão do IAM; papéis intermediários precisam ser configurados (§15).
| Permissão | Habilita | Separação que ela permite |
|---|---|---|
DOCUMENT_TEMPLATES_CREATE | Criar template | Quem escreve a minuta |
DOCUMENT_TEMPLATES_READ | Ler template e versões | Quem audita e quem consulta a redação vigente |
DOCUMENT_TEMPLATES_UPDATE | Alterar o rascunho | Quem revisa o texto jurídico |
DOCUMENT_TEMPLATES_DELETE | Excluir logicamente | Quem administra o catálogo |
DOCUMENT_TEMPLATES_PUBLISH | Congelar o rascunho em versão | O aprovador — o papel que a operação regulada separa do autor |
DOCUMENT_TEMPLATES_RENDER | Renderizar e pré-visualizar | A esteira que emite |
Exclusão lógica
Exclusão lógica. Templates usam deletedAt. Excluir um template não apaga as versões publicadas nem os documentos já emitidos — o histórico sobrevive à remoção, que é o que auditoria e obrigação de retenção exigem.
Enquadramento regulatório da CCB
Enquadramento. Para documentos de crédito, a CCB é regida pelos arts. 26 a 45 da Lei nº 10.931/2004. A lei admite expressamente a forma eletrônica: o art. 29, § 5º, incluído pela Lei nº 13.986/2020, estabelece que a assinatura do emitente "poderá ocorrer sob a forma eletrônica, desde que garantida a identificação inequívoca de seu signatário".
Duas consequências práticas, e é importante que elas fiquem separadas:
- A validação genérica de variável não é validação jurídica. O building block garante que toda variável declarada como obrigatória chegou preenchida. Ele não sabe o que são os seis requisitos essenciais do art. 29 e não confere se o seu template os contempla. Um
{{data_pagamento}}que você esqueceu de declarar como obrigatória sai vazio, e um requisito essencial em branco compromete a força executiva do título. A conferência da minuta é responsabilidade do seu jurídico — e vale ter um validador de requisitos essenciais na sua esteira, antes de chamar a renderização. - A identificação inequívoca do signatário não acontece aqui. Ela é assunto do e-signature, e a §14 dele explica com precisão o que é e o que não é garantido hoje. Este building block gera o documento; ele não assina, não autentica signatário e não produz prova de autoria.
Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
| O serviço de PDF não está publicado | PDF_RENDERER_URL não está definida em staging nem em produção, e não existe imagem, docker-compose ou manifesto do pdf-renderer no repositório. A rota /render/pdf devolve 400 nos ambientes publicados. | Bloqueante para PDF — só a saída em HTML funciona hoje |
| Não há endpoint para ler documentos emitidos | rendered_documents é gravada em toda renderização e nenhuma rota a expõe. Recuperar um documento emitido exige consulta direta ao banco | Roadmap — é a lacuna mais sentida em auditoria |
fileStorageId nunca é preenchido | A coluna existe na tabela e a integração com o File Storage não está implementada. O PDF volta em base64 e some se você não guardar | Roadmap |
ARCHIVED nunca é atribuído | O enum de status tem o valor, mas nenhuma rota o define. Arquivar template é, na prática, excluir logicamente | Especificado, não implementado |
| Renderização sempre usa a última versão publicada | Não há como pedir emissão com uma versão específica. Reproduzir um documento antigo exige recuperar o HTML gravado | Por design, mas limita reemissão |
| A pré-visualização não valida variáveis | POST /preview passa em casos que POST /render recusa. Usar a pré-visualização como ensaio dá falsa confiança | Por design — a pré-visualização existe para edição |
| Publicar não verifica se houve mudança | Chamadas repetidas criam versões idênticas com números diferentes, poluindo o histórico | Roadmap |
| Corpo limitado a 1 MB | Templates grandes de contrato, com CSS embutido e imagens em base64, batem no teto e recebem 413 | Por design do middleware comum; ajustável na infraestrutura |
| Sem saída em DOCX ou ODT | Só HTML e PDF. Quem precisa entregar arquivo editável ao jurídico não é atendido | Fora de escopo |
| Sem editor visual | O template é HTML escrito à mão. O jurídico depende de alguém que escreva HTML | Por design — é uma API |
| Sem retenção nem expurgo automatizado | Documentos emitidos com dado pessoal ficam indefinidamente. Atender pedido de eliminação da LGPD é processo manual | Roadmap |
| Sem numeração ou identificador de documento no conteúdo | O building block não gera número sequencial de contrato nem carimbo. Se o seu documento precisa disso, passe como variável | Fora de escopo |
| Não valida requisitos essenciais da CCB | A validação é genérica — presença e tipo das variáveis declaradas. O serviço não conhece os seis incisos do art. 29 da Lei nº 10.931/2004 e não avisa se o seu template deixou um de fora | Fora de escopo hoje; um validador de requisitos por categoria de documento está no roadmap |
| Sem hash nem carimbo de integridade do documento emitido | rendered_documents guarda o HTML, mas não um resumo criptográfico que prove que a linha não foi alterada depois. Para documento financeiro, integridade verificável costuma ser exigência | Roadmap |
Permissões só no papel ADMIN | DOCUMENT_TEMPLATES_* não está atribuída a nenhum papel intermediário no catálogo padrão do IAM. O papel DOCUMENT_OPERATOR recebe PDF_CCB_CREATE e PDF_TEMPLATE_RENDER, que nenhuma rota deste building block exige — são resquício de uma versão anterior | Configuração pendente no IAM |
| Sem formatação de número e data na engine | Handlebars não formata. {{valor}} imprime 15000, não 15.000,00. A formatação é responsabilidade de quem chama | Por design; auxiliares de formatação estão no roadmap |
| Sem cabeçalho e rodapé recorrentes | A paginação depende inteiramente do renderizador acompanhante e do CSS que você escrever | Depende da escolha do renderizador |
Perguntas frequentes
Consigo gerar PDF hoje?
Localmente sim, se você subir um serviço que aceite POST /api/v1/render-pdf com { html, options } e devolva o binário, e apontar PDF_RENDERER_URL para ele. Em staging e produção, não: a variável não está configurada e o serviço não está publicado. A rota responde 400 com PDF rendering is not configured. Trate a saída em HTML como o caminho suportado hoje e leia a §15 antes de prometer PDF em proposta.
Por que a rota é /document-templates/api/v1/document-templates?
Porque o basePath do app e o ponto de montagem dos routers repetem o nome do módulo. É feio e é real — o serviço responde nesse caminho. Copie do §9 em vez de deduzir.
Editar um template publicado quebra os contratos já emitidos?
Não. A edição altera o rascunho de trabalho. As versões publicadas são imutáveis e os documentos já emitidos não mudam. A alteração só passa a valer para novas renderizações depois de POST /publish — e mesmo assim, cada documento antigo continua apontando para a versão que o gerou.
Como sei qual redação um cliente recebeu?
Pelo templateVersionNumber que a renderização devolve. Guarde-o na sua operação. Depois, GET /:templateId/versions/:n traz o conteúdo exato daquela versão, a data de publicação e o changelog. Os valores usados estão em rendered_documents.inputData, hoje acessível apenas por consulta ao banco (§15).
Posso usar isto para gerar CCB de verdade?
A lei permite a forma eletrônica: o art. 29, § 5º da Lei nº 10.931/2004, incluído pela Lei nº 13.986/2020, admite assinatura eletrônica na cédula desde que garantida a identificação inequívoca do signatário. O building block gera o documento com a redação que você escrever e valida que todos os campos declarados chegaram — mas ele não confere os seis requisitos essenciais do art. 29, não numera, não registra em nenhuma central e não assina. A conformidade da redação é do seu jurídico; a identificação do signatário é do e-signature, e a §14 dele explica exatamente que nível de assinatura é produzido hoje. O seed do projeto traz uma CCB de exemplo com quinze variáveis, útil como ponto de partida — não como modelo jurídico aprovado.
O template pode ter lógica condicional?
Sim, dentro do que o Handlebars oferece: {{#if}}, {{#unless}}, {{#each}} e {{#with}}. Não há auxiliares personalizados registrados, então não há formatação de moeda, de data nem cálculo dentro do template. Se o documento precisa de um valor formatado, mande-o já formatado como STRING.
O que acontece se eu adicionar uma variável obrigatória a um template em uso?
Nada até você publicar. Depois de publicar, toda renderização que não enviar o campo novo passa a receber 400. Coordene a publicação com o deploy de quem chama, ou declare a variável com um default para que a transição seja suave.
Os dados do contrato saem da minha infraestrutura?
Não. A renderização acontece no processo, e a única chamada externa é ao renderizador de PDF, que é um serviço interno seu. Essa é a diferença material frente a um conversor de PDF em nuvem, onde o documento com CPF e condições financeiras precisa trafegar para um terceiro — o que exige base legal e contrato de operador sob a LGPD.
Qual é o limite de tamanho do template?
O corpo da requisição é limitado a 1 MB pelo middleware comum, o que cobre a esmagadora maioria dos contratos em HTML. Se você embutir imagens em base64 ou CSS extenso, esse teto chega rápido e a resposta é 413. Referencie imagens por URL em vez de embuti-las, ou ajuste o limite na infraestrutura.
Dá para o jurídico editar o template sem passar por engenharia?
Só se o jurídico escrever HTML, o que raramente é o caso. Este building block é uma API — não há editor. Quem precisa disso hoje monta uma interface própria em cima das rotas de PATCH e preview, ou avalia uma ferramenta de template em DOCX como o Carbone (§5). É uma limitação assumida.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md