feat(signatures): deriva sócios/representante automaticamente na criação do envelope
signers no CreateEnvelopeDto passa a ser opcional. Quando omitido, o contratante é derivado automaticamente da origem do envelope: representante legal autorizado (pj_rep_*/rep_*) + sócios marcados will_sign (client_registration_partners/reseller_partners), deduplicado por CPF; para cliente PF, o próprio titular. signers explícito continua funcionando como override manual completo, sem mudança de comportamento. Nunca falha silenciosamente: sócio marcado pra assinar sem e-mail de assinatura, ou nada disponível pra derivar, retorna 400 com mensagem clara em vez de criar um envelope incompleto. Suíte: 113/113 (107→113). pnpm turbo run typecheck build: 28/28. Testado ao vivo contra a API real. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,17 @@ export class CreateEnvelopeDto {
|
|||||||
@IsUUID("4", { each: true })
|
@IsUUID("4", { each: true })
|
||||||
documentGenerationIds!: string[];
|
documentGenerationIds!: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Omitted/empty → `contratante` signers are derived automatically from
|
||||||
|
* the envelope's origin (`clientRegistrationId`/`resellerId`): the
|
||||||
|
* legal representative (if authorized) plus every sócio marked
|
||||||
|
* `will_sign` — see `EnvelopeService.deriveContratanteSigners`. Provide
|
||||||
|
* this explicitly to override that derivation entirely (e.g. a signer
|
||||||
|
* not on file, or an origin-less envelope).
|
||||||
|
*/
|
||||||
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ArrayMinSize(1)
|
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => SignerInputDto)
|
@Type(() => SignerInputDto)
|
||||||
signers!: SignerInputDto[];
|
signers?: SignerInputDto[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
companiesRepo,
|
companiesRepo,
|
||||||
documentsRepo,
|
documentsRepo,
|
||||||
signaturesRepo,
|
signaturesRepo,
|
||||||
|
clientRegistrationsRepo,
|
||||||
|
clientRegistrationPartnersRepo,
|
||||||
|
resellersRepo,
|
||||||
|
resellerPartnersRepo,
|
||||||
type ActorType,
|
type ActorType,
|
||||||
type EnvelopeWithDetails,
|
type EnvelopeWithDetails,
|
||||||
type SignatureEnvelope,
|
type SignatureEnvelope,
|
||||||
@@ -33,6 +37,18 @@ export interface RequestContext {
|
|||||||
userAgent: string | null;
|
userAgent: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ContratanteSignerDraft {
|
||||||
|
party: "contratante";
|
||||||
|
personType: "pf" | "pj" | null;
|
||||||
|
fullName: string;
|
||||||
|
cpf: string | null;
|
||||||
|
email: string;
|
||||||
|
phone: string | null;
|
||||||
|
companyName: string | null;
|
||||||
|
cnpj: string | null;
|
||||||
|
roleTitle: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveActorType(roleKey: string): ActorType {
|
function resolveActorType(roleKey: string): ActorType {
|
||||||
if (roleKey === "super_admin") return "SUPER_ADMIN";
|
if (roleKey === "super_admin") return "SUPER_ADMIN";
|
||||||
if (roleKey === "admin") return "ADMIN";
|
if (roleKey === "admin") return "ADMIN";
|
||||||
@@ -119,6 +135,7 @@ export class EnvelopeService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contratanteSigners = await this.deriveContratanteSigners(dto);
|
||||||
const signers = [
|
const signers = [
|
||||||
{
|
{
|
||||||
party: "contratada" as const,
|
party: "contratada" as const,
|
||||||
@@ -127,17 +144,7 @@ export class EnvelopeService {
|
|||||||
email: company.legal_rep_email,
|
email: company.legal_rep_email,
|
||||||
roleTitle: company.legal_rep_role,
|
roleTitle: company.legal_rep_role,
|
||||||
},
|
},
|
||||||
...dto.signers.map((s) => ({
|
...contratanteSigners,
|
||||||
party: "contratante" as const,
|
|
||||||
personType: s.personType ?? null,
|
|
||||||
fullName: s.fullName,
|
|
||||||
cpf: s.cpf ?? null,
|
|
||||||
email: s.email,
|
|
||||||
phone: s.phone ?? null,
|
|
||||||
companyName: s.companyName ?? null,
|
|
||||||
cnpj: s.cnpj ?? null,
|
|
||||||
roleTitle: s.roleTitle ?? null,
|
|
||||||
})),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const envelope = await signaturesRepo.createEnvelope(
|
const envelope = await signaturesRepo.createEnvelope(
|
||||||
@@ -161,6 +168,157 @@ export class EnvelopeService {
|
|||||||
return envelope;
|
return envelope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `dto.signers` explícito (não vazio) sempre vence — override manual
|
||||||
|
* completo, mesmo comportamento de antes desta função existir. Vazio/
|
||||||
|
* omitido: deriva automaticamente da origem do envelope (usuário pediu
|
||||||
|
* isso depois de completar o cadastro de sócios/representante legal em
|
||||||
|
* client_registrations e resellers) — representante legal autorizado
|
||||||
|
* (`*_rep_authorization_ack`) + cada sócio marcado `will_sign`, dedup
|
||||||
|
* por CPF (o representante pode também constar como sócio). PF de
|
||||||
|
* client_registrations não tem sócios/representante — o próprio titular
|
||||||
|
* é o único signatário possível.
|
||||||
|
*/
|
||||||
|
private async deriveContratanteSigners(dto: CreateEnvelopeDto): Promise<ContratanteSignerDraft[]> {
|
||||||
|
if (dto.signers && dto.signers.length > 0) {
|
||||||
|
return dto.signers.map((s) => ({
|
||||||
|
party: "contratante" as const,
|
||||||
|
personType: s.personType ?? null,
|
||||||
|
fullName: s.fullName,
|
||||||
|
cpf: s.cpf ?? null,
|
||||||
|
email: s.email,
|
||||||
|
phone: s.phone ?? null,
|
||||||
|
companyName: s.companyName ?? null,
|
||||||
|
cnpj: s.cnpj ?? null,
|
||||||
|
roleTitle: s.roleTitle ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.clientRegistrationId) {
|
||||||
|
const registration = await clientRegistrationsRepo.getClientRegistrationById(dto.clientRegistrationId);
|
||||||
|
if (!registration) throw new NotFoundException("Cadastro de cliente não encontrado");
|
||||||
|
|
||||||
|
if (registration.person_type === "pf") {
|
||||||
|
if (!registration.pf_full_name || !registration.pf_email) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"O cadastro de cliente não tem nome/e-mail completos para assinar — complete o cadastro ou informe os signatários manualmente.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
party: "contratante" as const,
|
||||||
|
personType: "pf" as const,
|
||||||
|
fullName: registration.pf_full_name,
|
||||||
|
cpf: registration.pf_cpf,
|
||||||
|
email: registration.pf_email,
|
||||||
|
phone: registration.pf_mobile_phone,
|
||||||
|
companyName: null,
|
||||||
|
cnpj: null,
|
||||||
|
roleTitle: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const partners = await clientRegistrationPartnersRepo.listPartnersForRegistration(dto.clientRegistrationId);
|
||||||
|
return this.deriveFromRepAndPartners({
|
||||||
|
companyName: registration.pj_company_name,
|
||||||
|
cnpj: registration.pj_cnpj,
|
||||||
|
repFullName: registration.pj_rep_full_name,
|
||||||
|
repCpf: registration.pj_rep_cpf,
|
||||||
|
repRole: registration.pj_rep_role,
|
||||||
|
repEmail: registration.pj_rep_email,
|
||||||
|
repMobilePhone: registration.pj_rep_mobile_phone,
|
||||||
|
repAuthorizationAck: registration.pj_rep_authorization_ack,
|
||||||
|
partners,
|
||||||
|
originLabel: "cadastro de cliente",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// `hasReseller` — a XOR de clientRegistrationId/resellerId já foi validada por quem chama.
|
||||||
|
const reseller = await resellersRepo.getResellerById(dto.resellerId!);
|
||||||
|
if (!reseller) throw new NotFoundException("Revenda não encontrada");
|
||||||
|
const partners = await resellerPartnersRepo.listPartnersForReseller(dto.resellerId!);
|
||||||
|
return this.deriveFromRepAndPartners({
|
||||||
|
companyName: reseller.name,
|
||||||
|
cnpj: reseller.document,
|
||||||
|
repFullName: reseller.rep_full_name,
|
||||||
|
repCpf: reseller.rep_cpf,
|
||||||
|
repRole: reseller.rep_role,
|
||||||
|
repEmail: reseller.rep_email,
|
||||||
|
repMobilePhone: reseller.rep_mobile_phone,
|
||||||
|
repAuthorizationAck: reseller.rep_authorization_ack,
|
||||||
|
partners,
|
||||||
|
originLabel: "revenda",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private deriveFromRepAndPartners(input: {
|
||||||
|
companyName: string | null;
|
||||||
|
cnpj: string | null;
|
||||||
|
repFullName: string | null;
|
||||||
|
repCpf: string | null;
|
||||||
|
repRole: string | null;
|
||||||
|
repEmail: string | null;
|
||||||
|
repMobilePhone: string | null;
|
||||||
|
repAuthorizationAck: boolean;
|
||||||
|
partners: Array<{
|
||||||
|
name: string;
|
||||||
|
cpf: string | null;
|
||||||
|
qualification: string | null;
|
||||||
|
will_sign: boolean;
|
||||||
|
signature_email: string | null;
|
||||||
|
}>;
|
||||||
|
originLabel: string;
|
||||||
|
}): ContratanteSignerDraft[] {
|
||||||
|
const signers: ContratanteSignerDraft[] = [];
|
||||||
|
const seenCpfs = new Set<string>();
|
||||||
|
|
||||||
|
if (input.repAuthorizationAck && input.repFullName && input.repEmail) {
|
||||||
|
signers.push({
|
||||||
|
party: "contratante",
|
||||||
|
personType: "pf",
|
||||||
|
fullName: input.repFullName,
|
||||||
|
cpf: input.repCpf,
|
||||||
|
email: input.repEmail,
|
||||||
|
phone: input.repMobilePhone,
|
||||||
|
companyName: input.companyName,
|
||||||
|
cnpj: input.cnpj,
|
||||||
|
roleTitle: input.repRole,
|
||||||
|
});
|
||||||
|
if (input.repCpf) seenCpfs.add(input.repCpf);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const partner of input.partners) {
|
||||||
|
if (!partner.will_sign) continue;
|
||||||
|
if (partner.cpf && seenCpfs.has(partner.cpf)) continue; // já coberto pelo representante
|
||||||
|
if (!partner.signature_email) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`O sócio "${partner.name}" está marcado para assinar mas não tem e-mail de assinatura cadastrado — complete o cadastro (${input.originLabel}) ou informe os signatários manualmente.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
signers.push({
|
||||||
|
party: "contratante",
|
||||||
|
personType: "pf",
|
||||||
|
fullName: partner.name,
|
||||||
|
cpf: partner.cpf,
|
||||||
|
email: partner.signature_email,
|
||||||
|
phone: null,
|
||||||
|
companyName: input.companyName,
|
||||||
|
cnpj: input.cnpj,
|
||||||
|
roleTitle: partner.qualification,
|
||||||
|
});
|
||||||
|
if (partner.cpf) seenCpfs.add(partner.cpf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signers.length === 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Nenhum sócio marcado para assinar nem representante legal autorizado no cadastro (${input.originLabel}) — marque quem assina no cadastro ou informe os signatários manualmente nesta requisição.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signers;
|
||||||
|
}
|
||||||
|
|
||||||
/** eden.md §5.5: gera um token novo por signatário-alvo ainda não assinado (revoga implicitamente o anterior), 7 dias de validade, e-mail com o link. Envelope READY->SENT na primeira vez. */
|
/** eden.md §5.5: gera um token novo por signatário-alvo ainda não assinado (revoga implicitamente o anterior), 7 dias de validade, e-mail com o link. Envelope READY->SENT na primeira vez. */
|
||||||
async sendInvite(id: string, dto: SendInviteDto, user: EdenAuthenticatedUser): Promise<EnvelopeWithDetails> {
|
async sendInvite(id: string, dto: SendInviteDto, user: EdenAuthenticatedUser): Promise<EnvelopeWithDetails> {
|
||||||
const envelope = await this.get(id, user);
|
const envelope = await this.get(id, user);
|
||||||
|
|||||||
302
apps/api/test/signature-envelope-auto-signers.spec.ts
Normal file
302
apps/api/test/signature-envelope-auto-signers.spec.ts
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
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", plainText: "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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user