feat(api,web): fundação de Documentos + EDEN Parceiros/Assinante com login real

Continuação da leva "pelo menos mais 20 etapas" — 2 subagentes em paralelo.

## Documents (apps/api/src/documents/ + migration documents)

Metade "templates + geração" do eden.md módulo 4 (§1.1-1.3): document_templates,
document_template_versions (ciclo draft→publish, no máximo 1 rascunho por
template via índice único parcial, versão publicada imutável), document_generations
(idempotente por (template_version_id, origem), CHECK de origem única). Motor de
merge de variáveis ({{chave}}→valor, nunca lança exceção) como função pura
testável. A metade de Assinatura Eletrônica (envelopes, signatários, OTP,
cadeia de auditoria hash-chain — eden.md §1.4-1.9/§5-7, 9 tabelas satélite)
fica deliberadamente fora — grande e sensível demais para dividir espaço
com esta leva, fica para uma leva própria. PDF real via Chromium também
deferido; document_generations grava rendered_html, pdf_* ficam NULL.

## EDEN Parceiros / EDEN Assinante (apps/reseller-web, apps/subscriber-web)

Replicam o shell autenticado do core-web (leva anterior): login real com
suporte a MFA, sessão, AppShell do Design System — trocando só
applicationKey ("reseller"/"subscriber"), título e chave de localStorage
da sessão. As duas telas "Bootstrap da Fase 1" saem de circulação.

Testado ao vivo contra os containers reais: ciclo completo de template
(criar→draft→409 no segundo draft→editar→publicar→gerar documento com
merge de variáveis funcionando→regeneração idempotente reaproveita o
registro), e login via /v1/auth/login com applicationKey "reseller"/
"subscriber" retornando tokens válidos nos 2 portais reconstruídos.
pnpm turbo run typecheck build (28/28), pnpm --filter @eden/api test
(30/30, sem regressão), trilha de auditoria conferida no Postgres.

Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 17:55:52 -03:00
parent eff515b70d
commit 4d67fe5f1a
42 changed files with 2242 additions and 20 deletions

View File

@@ -0,0 +1,49 @@
/**
* Motor de merge de variáveis — versão SIMPLIFICADA para esta leva,
* inspirada no `resolveDocument()` do legado (eden.md §3.2), mas
* operando sobre TEXTO PURO com placeholders `{{variavel}}` em vez da
* árvore Tiptap completa. A árvore Tiptap real (nós `mergeField`,
* `conditional`, `tableRow.attrs.repeat`, catálogo de variáveis
* `VARIABLE_CATALOG` com formatação por tipo — eden.md §2.2, §3.1, §3.3,
* §3.4) fica para quando o editor visual real existir; não é
* reimplementada aqui.
*
* Pura e sem I/O — testável isoladamente (ver
* apps/api/test/merge-engine.spec.ts). Diferença deliberada em relação
* ao motor completo do legado: aquele lança `MISSING_REQUIRED_FIELDS`/
* `UNRESOLVED_VARIABLES` (HTTP 422) e bloqueia a geração; este NUNCA
* lança exceção — variáveis ausentes do contexto são substituídas por
* string vazia e apenas reportadas em `missingVariables` (deduplicadas,
* na ordem da primeira ocorrência). O chamador decide o que fazer com
* elas (bloquear, avisar, etc.); o motor não decide isso sozinho.
*/
export interface MergeResult {
/** Texto com todo `{{variavel}}` substituído — nunca sobra placeholder pendente, mesmo quando ausente do contexto (vira string vazia). */
resolved: string;
/** Chaves referenciadas no texto mas ausentes de `context`, deduplicadas, na ordem da primeira ocorrência. */
missingVariables: string[];
}
// Mesma sintaxe de chave do catálogo legado (eden.md §3.1): namespace.campo,
// ex. "cliente.razao_social". Espaços em volta da chave são tolerados
// ("{{ cliente.razao_social }}") e ignorados.
const MERGE_FIELD_PATTERN = /\{\{\s*([A-Za-z0-9_.]+)\s*\}\}/g;
export function mergeVariables(text: string, context: Record<string, string>): MergeResult {
const missingVariables: string[] = [];
const seenMissing = new Set<string>();
const resolved = text.replace(MERGE_FIELD_PATTERN, (_match: string, key: string) => {
if (Object.prototype.hasOwnProperty.call(context, key)) {
return context[key] ?? "";
}
if (!seenMissing.has(key)) {
seenMissing.add(key);
missingVariables.push(key);
}
return "";
});
return { resolved, missingVariables };
}