From 3785b32c5f0c989ec85097f1ea0c9d78b8649e4b Mon Sep 17 00:00:00 2001 From: "Matheus (Handix)" Date: Sat, 19 Sep 2026 20:26:18 -0300 Subject: [PATCH] =?UTF-8?q?feat(documents):=20suporte=20a=20linha=20condic?= =?UTF-8?q?ional=20(conditional)=20+=20cat=C3=A1logo=20completo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Necessário pra importar os 7 templates reais do legado (fornecidos pelo usuário via export do OrçaFácil): o Termo de Contratação STFC usa o nó `conditional` (envelope em volta de uma tableRow, franquia de minutos/0800) — sem suporte, tanto o backend quanto o editor Tiptap rejeitavam/renderizavam em branco esse documento específico, mesmo bug já documentado no export do legado. A condição em si nunca é avaliada nesta leva (fora de escopo) — a linha sempre passa adiante, comportamento explícito e documentado. Também adiciona ao variable-catalog.ts as ~25 variáveis que faltavam (tarifação STFC por banda, 0800, portabilidade, prazo de instalação), descobertas comparando contra as chaves reais usadas nos 7 templates importados. Co-Authored-By: Claude Sonnet 5 --- .../src/documents/editor-content-renderer.ts | 19 ++++-- apps/api/src/documents/variable-catalog.ts | 22 +++++++ apps/api/test/editor-content-renderer.spec.ts | 50 ++++++++++++++++ .../documents/editor/DocumentEditor.tsx | 13 +++- .../editor/conditionalRowExtension.ts | 59 +++++++++++++++++++ docs/progress.md | 58 ++++++++++++++++++ 6 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 apps/core-web/src/routes/documents/editor/conditionalRowExtension.ts diff --git a/apps/api/src/documents/editor-content-renderer.ts b/apps/api/src/documents/editor-content-renderer.ts index 53690c7..24d80a3 100644 --- a/apps/api/src/documents/editor-content-renderer.ts +++ b/apps/api/src/documents/editor-content-renderer.ts @@ -7,11 +7,13 @@ * * Cobre os nós que o editor visual (apps/core-web/src/routes/documents/editor) * realmente produz: paragraph/heading/bulletList/orderedList/listItem/image/ - * pageBreak/table+tableRow+tableHeader+tableCell/mergeField, marks bold/ - * italic/underline/strike/textStyle(fontSize). NÃO implementado (fora de - * escopo desta leva, ver docs/progress.md): nó `conditional` e - * `tableRow.attrs.repeat` (expansão de linha de tabela) — o editor/catálogo - * de variáveis desta leva nunca produz nenhum dos dois. + * pageBreak/table+tableRow+tableHeader+tableCell/mergeField/conditional, + * marks bold/italic/underline/strike/textStyle(fontSize). `conditional` + * (envelope em volta de uma `tableRow`, usado pelo Termo de Contratação + * STFC importado do legado) sempre RENDERIZA sua linha — avaliação real da + * condição contra o contexto é fora de escopo desta leva (ver + * docs/progress.md). NÃO implementado: `tableRow.attrs.repeat` (expansão de + * linha de tabela) — nenhum dos 7 templates importados do legado usa. */ export interface TiptapMark { @@ -117,6 +119,13 @@ function renderBlock(node: TiptapNode, resolveMergeField?: ResolveMergeField): s const colspan = typeof node.attrs?.colspan === "number" ? ` colspan="${node.attrs.colspan}"` : ""; return `${(node.content ?? []).map((c) => renderBlock(c, resolveMergeField)).join("")}`; } + case "conditional": + // Envelope em volta de uma `tableRow` (attrs: field/operator/value) — + // avaliação real da condição contra o contexto (eden.md §3.2, + // `applyConditionals`) é fora de escopo desta leva (ver + // docs/progress.md): a linha sempre passa adiante, igual ao + // comportamento do próprio legado quando não avaliada. + return (node.content ?? []).map((c) => renderBlock(c, resolveMergeField)).join(""); default: return ""; } diff --git a/apps/api/src/documents/variable-catalog.ts b/apps/api/src/documents/variable-catalog.ts index b165985..dc57177 100644 --- a/apps/api/src/documents/variable-catalog.ts +++ b/apps/api/src/documents/variable-catalog.ts @@ -101,6 +101,21 @@ export const VARIABLE_CATALOG: VariableCatalogEntry[] = [ { key: "oferta.franquia_minutos", label: "Franquia de Minutos", category: "Oferta", required: false }, { key: "oferta.desconto_fidelidade", label: "Desconto por Fidelidade", category: "Oferta", required: false }, { key: "oferta.total_beneficios", label: "Total de Benefícios (VTB)", category: "Oferta", required: false }, + { key: "oferta.comercializacao_inicio", label: "Início do Prazo de Comercialização", category: "Oferta", required: false }, + { key: "oferta.comercializacao_fim", label: "Fim do Prazo de Comercialização", category: "Oferta", required: false }, + + // 0800 — sem cadastro próprio, preenchidos manualmente na geração. + { key: "oferta.inclui_0800", label: "Inclui Número 0800? (Sim/Não)", category: "Oferta", required: false }, + { key: "oferta.numero_0800", label: "Número 0800", category: "Oferta", required: false }, + { key: "oferta.franquia_0800_min", label: "Franquia Mensal de 0800 (min)", category: "Oferta", required: false }, + { key: "oferta.valor_0800_padrao", label: "Valor 0800 — Padrão (por minuto)", category: "Oferta", required: false }, + { key: "oferta.valor_0800_reduzido", label: "Valor 0800 — Reduzido (por minuto)", category: "Oferta", required: false }, + + // Tabela de tarifação STFC — automática a partir de products.tariff_rates do produto medido da oferta. + ...(["lc", "ldn", "vc1", "vc2", "vc3", "ldi"] as const).flatMap((t) => [ + { key: `oferta.tarifa_${t}_normal`, label: `Tarifa ${t.toUpperCase()} — Normal (por minuto)`, category: "Oferta", required: false }, + { key: `oferta.tarifa_${t}_reduzida`, label: `Tarifa ${t.toUpperCase()} — Reduzida (por minuto)`, category: "Oferta", required: false }, + ]), // CONTRATO — client_registrations.ixc_contract_number + quotes.contract_period/fidelity_period. { key: "contrato.numero", label: "Número do Contrato IXC", category: "Contrato", required: false }, @@ -111,6 +126,7 @@ export const VARIABLE_CATALOG: VariableCatalogEntry[] = [ { key: "contrato.software_nome", label: "Nome do Software", category: "Contrato", required: false }, { key: "contrato.numeros_portar", label: "Números a Portar", category: "Contrato", required: false }, { key: "contrato.quantidade_portar", label: "Quantidade de Números a Portar", category: "Contrato", required: false }, + { key: "contrato.prazo_instalacao", label: "Prazo de Instalação", category: "Contrato", required: false }, // PORTABILIDADE / CESSÃO — sem cadastro próprio, preenchidos manualmente na geração. { key: "portabilidade.prestadora_doadora", label: "Prestadora Doadora", category: "Portabilidade", required: false }, @@ -121,6 +137,12 @@ export const VARIABLE_CATALOG: VariableCatalogEntry[] = [ category: "Portabilidade", required: false, }, + { + key: "portabilidade.titular_cpf_cnpj_doadora", + label: "CNPJ/CPF do Titular na Doadora", + category: "Portabilidade", + required: false, + }, { key: "cessao.codigo_acesso_cedido", label: "Código(s) de Acesso Cedido(s)", category: "Cessão", required: false }, { key: "cessao.numeros_contratados", label: "Números Contratados", category: "Cessão", required: false }, diff --git a/apps/api/test/editor-content-renderer.spec.ts b/apps/api/test/editor-content-renderer.spec.ts index 73cda29..b36bd78 100644 --- a/apps/api/test/editor-content-renderer.spec.ts +++ b/apps/api/test/editor-content-renderer.spec.ts @@ -116,6 +116,56 @@ describe("editor-content-renderer", () => { expect(resolvedHtml).not.toContain("merge-field-chip"); }); + it("conditional sempre renderiza sua linha (avaliação real de condição é trabalho futuro)", () => { + const doc = { + type: "doc", + content: [ + { + type: "table", + content: [ + { + type: "tableRow", + content: [{ type: "tableHeader", content: [{ type: "paragraph", content: [{ type: "text", text: "Coluna" }] }] }], + }, + { + type: "conditional", + attrs: { field: "oferta.franquia_minutos", operator: "exists" }, + content: [ + { + type: "tableRow", + content: [ + { type: "tableCell", content: [{ type: "paragraph", content: [{ type: "text", text: "linha condicional" }] }] }, + ], + }, + ], + }, + ], + }, + ], + }; + + const html = renderToHtml(doc); + expect(html).toMatch(/.*

Coluna<\/p><\/th>.*<\/thead>/s); + expect(html).toContain("

linha condicional

"); + expect(html).not.toContain("conditional"); + }); + + it("collectMergeFieldKeys enxerga chaves dentro de um conditional", () => { + const doc = { + type: "doc", + content: [ + { + type: "conditional", + attrs: { field: "x" }, + content: [ + { type: "tableRow", content: [{ type: "tableCell", content: [{ type: "mergeField", attrs: { key: "a.b" } }] }] }, + ], + }, + ], + }; + expect(collectMergeFieldKeys(doc)).toEqual(["a.b"]); + }); + it("escapa HTML de texto e de valores resolvidos (proteção contra XSS)", () => { const doc = { type: "doc", diff --git a/apps/core-web/src/routes/documents/editor/DocumentEditor.tsx b/apps/core-web/src/routes/documents/editor/DocumentEditor.tsx index fe626e2..f1ff676 100644 --- a/apps/core-web/src/routes/documents/editor/DocumentEditor.tsx +++ b/apps/core-web/src/routes/documents/editor/DocumentEditor.tsx @@ -13,8 +13,18 @@ import { TableCell } from "@tiptap/extension-table-cell"; import type { VariableCatalogEntry } from "../../../lib/api/documents.js"; import { MergeField } from "./mergeFieldExtension.js"; import { PageBreak } from "./pageBreakExtension.js"; +import { ConditionalRow } from "./conditionalRowExtension.js"; import { EditorToolbar } from "./EditorToolbar.js"; +/** + * `Table` precisa aceitar `conditional` como filho direto, além de + * `tableRow` — sem isso o Tiptap rejeita o documento inteiro (renderiza em + * branco) sempre que uma tabela contém uma linha condicional, exatamente o + * bug documentado no export dos templates do legado (README dentro de + * `document_templates_export.zip`) e já corrigido lá da mesma forma. + */ +const TableWithConditional = Table.extend({ content: "(tableRow|conditional)+" }); + /** * Editor de rascunho real (Tiptap) — substitui o `