Files
eden/apps/api/test/signature-envelope-auto-signers.spec.ts
Matheus (Handix) 1545666f17 feat(documents): editor de texto rico (Tiptap) para templates de documento
Substitui o textarea de texto puro por um editor visual real, seguindo o
print de referência do usuário do editor "Contrato de Software" do
OrçaFácil legado: toolbar completa (negrito/itálico/sublinhado/tachado,
alinhamento, listas, tabela, imagem, quebra de página), variáveis inseridas
como chip inline via catálogo fechado (nunca {{texto livre}}), painel
"Variáveis usadas neste modelo" (com badge obrigatório ao vivo) e
"Histórico de versões".

Backend: editor-content-renderer.ts e merge-engine.ts portados/reescritos
a partir do renderEditorContent.js/variableRegistry.js reais do legado
(achados em .tmp/orcafacil-reference) — resolve a árvore Tiptap
diretamente em vez de regex sobre HTML cacheado. Novo catálogo de
variáveis + GET /documents/variable-catalog.

Fora de escopo desta leva (documentado em docs/progress.md): nó
`conditional`, repetição de linha de tabela, bloqueio por variável
obrigatória ausente, e auto-resolução de dados reais do cadastro na hora
de gerar o documento.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 19:49:42 -03:00

305 lines
12 KiB
TypeScript

import { INestApplication } from "@nestjs/common";
import { Readable } from "node:stream";
import { PDFDocument } from "pdf-lib";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, qaEmail, qaName, type AuthedTokens } from "./setup.js";
/**
* `POST /v1/signature-envelopes` with `signers` omitted — contratante
* signers derived automatically from the envelope's origin
* (`clientRegistrationId`/`resellerId`): the legal representative (if
* `*_rep_authorization_ack`) plus every sócio marked `will_sign`, deduped
* by CPF. Mocks match `signature-envelope-flow.spec.ts` exactly (no real
* Chromium/MinIO/SMTP needed).
*/
const s3Store = new Map<string, Buffer>();
vi.mock("@eden/integrations", async () => {
const actual = await vi.importActual<typeof import("@eden/integrations")>("@eden/integrations");
return {
...actual,
renderPdf: vi.fn(async () => {
const doc = await PDFDocument.create();
doc.addPage();
return Buffer.from(await doc.save());
}),
uploadObject: vi.fn(async (key: string, buffer: Buffer) => {
s3Store.set(key, buffer);
}),
getObjectStream: vi.fn(async (key: string) => {
const buffer = s3Store.get(key);
if (!buffer) throw new Error(`qa mock S3: no object at ${key}`);
return Readable.from([buffer]);
}),
sendMail: vi.fn(async () => undefined),
};
});
describe("signature envelopes — automatic contratante signer derivation", () => {
let app: INestApplication;
let admin: AuthedTokens;
let companyId: string;
let generationId: string;
async function createGeneration(resellerId?: string, clientRegistrationId?: string) {
const template = await agent(app)
.post("/api/v1/documents/templates")
.set(...bearer(admin.accessToken))
.send({ key: `qa_test_autosig_${Date.now()}_${Math.random().toString(36).slice(2)}`, name: qaName("Template AutoSigners"), docGroup: "revenda" })
.expect(201);
const templateId = template.body.id;
await agent(app).post(`/api/v1/documents/templates/${templateId}/draft`).set(...bearer(admin.accessToken)).expect(201);
await agent(app)
.patch(`/api/v1/documents/templates/${templateId}/draft`)
.set(...bearer(admin.accessToken))
.send({
editorContent: { type: "doc", content: [{ type: "paragraph", content: [{ type: "text", text: "Contrato de teste." }] }] },
})
.expect(200);
await agent(app).post(`/api/v1/documents/templates/${templateId}/publish`).set(...bearer(admin.accessToken)).expect(201);
const generation = await agent(app)
.post("/api/v1/documents/generations")
.set(...bearer(admin.accessToken))
.send({ templateId, resellerId, clientRegistrationId, context: {} })
.expect(201);
return generation.body.id as string;
}
beforeAll(async () => {
app = await createTestApp();
admin = await loginSuperAdmin(app);
const company = await agent(app)
.post("/api/v1/companies")
.set(...bearer(admin.accessToken))
.send({
companyName: qaName("Empresa AutoSigners"),
legalRepName: "Representante Legal QA",
legalRepCpf: "11144477735",
legalRepEmail: qaEmail("autosig-contratada"),
legalRepRole: "Diretor",
})
.expect(201);
companyId = company.body.id;
});
afterAll(async () => {
await closeTestApp(app);
});
describe("origem client_registrations (PJ)", () => {
let registrationId: string;
beforeAll(async () => {
const reg = await agent(app)
.post("/api/v1/client-registrations/direct")
.set(...bearer(admin.accessToken))
.send({ personType: "pj", isPortability: false })
.expect(201);
registrationId = reg.body.id;
generationId = await createGeneration(undefined, registrationId);
});
it("400 — sem representante autorizado e sem sócio marcado pra assinar, nada pra derivar", async () => {
const res = await agent(app)
.post("/api/v1/signature-envelopes")
.set(...bearer(admin.accessToken))
.send({ companyId, clientRegistrationId: registrationId, documentGenerationIds: [generationId] })
.expect(400);
expect(res.body.message).toMatch(/Nenhum sócio marcado|representante legal autorizado/);
});
it("400 — sócio marcado pra assinar mas sem e-mail de assinatura", async () => {
await agent(app)
.post(`/api/v1/client-registrations/${registrationId}/partners`)
.set(...bearer(admin.accessToken))
.send({ name: qaName("Sócio Sem Email"), willSign: true })
.expect(201);
const res = await agent(app)
.post("/api/v1/signature-envelopes")
.set(...bearer(admin.accessToken))
.send({ companyId, clientRegistrationId: registrationId, documentGenerationIds: [generationId] })
.expect(400);
expect(res.body.message).toContain("e-mail de assinatura");
});
it("deriva o representante legal autorizado + o sócio marcado pra assinar, ignora o não-marcado", async () => {
// Cadastro próprio, dedicado — o `registrationId` do describe já
// acumulou, nos testes anteriores, um sócio `will_sign=true` sem
// e-mail (criado de propósito pra testar o 400) que continuaria
// presente e derrubaria este teste por um motivo alheio ao que ele
// verifica.
const reg = await agent(app)
.post("/api/v1/client-registrations/direct")
.set(...bearer(admin.accessToken))
.send({ personType: "pj", isPortability: false })
.expect(201);
const freshRegistrationId = reg.body.id;
const freshGenerationId = await createGeneration(undefined, freshRegistrationId);
const repEmail = qaEmail("autosig-pj-rep");
await agent(app)
.patch(`/api/v1/client-registrations/${freshRegistrationId}`)
.set(...bearer(admin.accessToken))
.send({
pjRepFullName: "Representante PJ QA",
pjRepRole: "Sócio-Administrador",
pjRepEmail: repEmail,
pjRepMobilePhone: "11988887777",
pjRepAuthorizationAck: true,
})
.expect(200);
const willSignEmail = qaEmail("autosig-pj-partner");
await agent(app)
.post(`/api/v1/client-registrations/${freshRegistrationId}/partners`)
.set(...bearer(admin.accessToken))
.send({ name: qaName("Sócio Assina"), willSign: true, signatureEmail: willSignEmail, qualification: "Sócio" })
.expect(201);
await agent(app)
.post(`/api/v1/client-registrations/${freshRegistrationId}/partners`)
.set(...bearer(admin.accessToken))
.send({ name: qaName("Sócio Não Assina") })
.expect(201);
const res = await agent(app)
.post("/api/v1/signature-envelopes")
.set(...bearer(admin.accessToken))
.send({ companyId, clientRegistrationId: freshRegistrationId, documentGenerationIds: [freshGenerationId] })
.expect(201);
const contratantes = res.body.signers.filter((s: { party: string }) => s.party === "contratante");
expect(contratantes).toHaveLength(2); // rep + o único sócio marcado pra assinar
const emails = contratantes.map((s: { email: string }) => s.email);
expect(emails).toContain(repEmail);
expect(emails).toContain(willSignEmail);
});
});
describe("origem client_registrations (PF)", () => {
it("usa o próprio titular como signatário único", async () => {
const reg = await agent(app)
.post("/api/v1/client-registrations/direct")
.set(...bearer(admin.accessToken))
.send({ personType: "pf", isPortability: false })
.expect(201);
const registrationId = reg.body.id;
const titularEmail = qaEmail("autosig-pf-titular");
await agent(app)
.patch(`/api/v1/client-registrations/${registrationId}`)
.set(...bearer(admin.accessToken))
.send({ pfFullName: "Titular PF QA", pfEmail: titularEmail, pfMobilePhone: "11977776666" })
.expect(200);
const genId = await createGeneration(undefined, registrationId);
const res = await agent(app)
.post("/api/v1/signature-envelopes")
.set(...bearer(admin.accessToken))
.send({ companyId, clientRegistrationId: registrationId, documentGenerationIds: [genId] })
.expect(201);
const contratantes = res.body.signers.filter((s: { party: string }) => s.party === "contratante");
expect(contratantes).toHaveLength(1);
expect(contratantes[0].email).toBe(titularEmail);
expect(contratantes[0].full_name).toBe("Titular PF QA");
});
});
describe("origem resellers", () => {
let resellerId: string;
beforeAll(async () => {
const reseller = await agent(app)
.post("/api/v1/resellers")
.set(...bearer(admin.accessToken))
.send({ name: qaName("Revenda AutoSigners") })
.expect(201);
resellerId = reseller.body.id;
generationId = await createGeneration(resellerId);
});
afterAll(async () => {
await agent(app)
.patch(`/api/v1/resellers/${resellerId}`)
.set(...bearer(admin.accessToken))
.send({ active: false })
.expect(200);
});
it("dedup por CPF — sócio com o mesmo CPF do representante aparece só uma vez", async () => {
const repCpf = "52998224725";
const repEmail = qaEmail("autosig-reseller-rep");
await agent(app)
.patch(`/api/v1/resellers/${resellerId}`)
.set(...bearer(admin.accessToken))
.send({
repFullName: "Representante Revenda QA",
repCpf,
repRole: "Diretor",
repEmail,
repAuthorizationAck: true,
})
.expect(200);
// Mesmo CPF do representante — não deve virar um segundo signatário.
await agent(app)
.post(`/api/v1/resellers/${resellerId}/partners`)
.set(...bearer(admin.accessToken))
.send({ name: "Representante Revenda QA", cpf: repCpf, willSign: true, signatureEmail: qaEmail("autosig-reseller-dup") })
.expect(201);
// (`toHaveLength(2)` abaixo já prova o dedup — rep + o único outro sócio, nunca 3.)
const otherEmail = qaEmail("autosig-reseller-other");
await agent(app)
.post(`/api/v1/resellers/${resellerId}/partners`)
.set(...bearer(admin.accessToken))
.send({ name: qaName("Outro Sócio Revenda"), cpf: "12345678909", willSign: true, signatureEmail: otherEmail })
.expect(201);
const res = await agent(app)
.post("/api/v1/signature-envelopes")
.set(...bearer(admin.accessToken))
.send({ companyId, resellerId, documentGenerationIds: [generationId] })
.expect(201);
const contratantes = res.body.signers.filter((s: { party: string }) => s.party === "contratante");
expect(contratantes).toHaveLength(2); // rep (dedup'd against the same-CPF partner) + the other partner
const emails = contratantes.map((s: { email: string }) => s.email);
expect(emails).toContain(repEmail);
expect(emails).toContain(otherEmail);
});
});
it("signers explícito continua funcionando como override manual completo, mesmo com rep/sócios disponíveis", async () => {
const reg = await agent(app)
.post("/api/v1/client-registrations/direct")
.set(...bearer(admin.accessToken))
.send({ personType: "pj", isPortability: false })
.expect(201);
const registrationId = reg.body.id;
await agent(app)
.patch(`/api/v1/client-registrations/${registrationId}`)
.set(...bearer(admin.accessToken))
.send({ pjRepFullName: "Rep Ignorado", pjRepEmail: qaEmail("autosig-override-ignored"), pjRepAuthorizationAck: true })
.expect(200);
const genId = await createGeneration(undefined, registrationId);
const manualEmail = qaEmail("autosig-override-manual");
const res = await agent(app)
.post("/api/v1/signature-envelopes")
.set(...bearer(admin.accessToken))
.send({
companyId,
clientRegistrationId: registrationId,
documentGenerationIds: [genId],
signers: [{ fullName: "Signatário Manual", email: manualEmail }],
})
.expect(201);
const contratantes = res.body.signers.filter((s: { party: string }) => s.party === "contratante");
expect(contratantes).toHaveLength(1);
expect(contratantes[0].email).toBe(manualEmail);
});
});