diff --git a/apps/api/src/resellers/dto/create-reseller.dto.ts b/apps/api/src/resellers/dto/create-reseller.dto.ts index 2f8ef37..f29e41b 100644 --- a/apps/api/src/resellers/dto/create-reseller.dto.ts +++ b/apps/api/src/resellers/dto/create-reseller.dto.ts @@ -35,4 +35,15 @@ export class CreateResellerDto { @IsOptional() @IsString() contactTechnicalName?: string; @IsOptional() @IsEmail() contactTechnicalEmail?: string; @IsOptional() @IsString() contactTechnicalPhone?: string; + + // Representante legal — usado depois para confecção de contrato com a + // revenda (não bloqueado no PATCH genérico, ao contrário do equivalente + // em client_registrations: ver o cabeçalho da migration + // 1789700100000_resellers-legal-representative.cjs para o porquê. + @IsOptional() @IsString() repFullName?: string; + @IsOptional() @IsString() repCpf?: string; + @IsOptional() @IsString() repRole?: string; + @IsOptional() @IsEmail() repEmail?: string; + @IsOptional() @IsString() repMobilePhone?: string; + @IsOptional() @IsBoolean() repAuthorizationAck?: boolean; } diff --git a/apps/api/src/resellers/resellers.service.ts b/apps/api/src/resellers/resellers.service.ts index ff7ab0a..2bf6b4b 100644 --- a/apps/api/src/resellers/resellers.service.ts +++ b/apps/api/src/resellers/resellers.service.ts @@ -70,6 +70,12 @@ export class ResellersService { contactTechnicalName: dto.contactTechnicalName, contactTechnicalEmail: dto.contactTechnicalEmail, contactTechnicalPhone: dto.contactTechnicalPhone, + repFullName: dto.repFullName, + repCpf: dto.repCpf, + repRole: dto.repRole, + repEmail: dto.repEmail, + repMobilePhone: dto.repMobilePhone, + repAuthorizationAck: dto.repAuthorizationAck, }); await auditRepo.appendAuditEvent({ @@ -116,6 +122,12 @@ export class ResellersService { contactTechnicalName: dto.contactTechnicalName, contactTechnicalEmail: dto.contactTechnicalEmail, contactTechnicalPhone: dto.contactTechnicalPhone, + repFullName: dto.repFullName, + repCpf: dto.repCpf, + repRole: dto.repRole, + repEmail: dto.repEmail, + repMobilePhone: dto.repMobilePhone, + repAuthorizationAck: dto.repAuthorizationAck, }); if (!reseller) throw new NotFoundException("Revenda não encontrada"); diff --git a/apps/api/test/resellers-cnpj-lookup.spec.ts b/apps/api/test/resellers-cnpj-lookup.spec.ts index 5a3c4cc..afbb86f 100644 --- a/apps/api/test/resellers-cnpj-lookup.spec.ts +++ b/apps/api/test/resellers-cnpj-lookup.spec.ts @@ -211,4 +211,32 @@ describe("resellers — CNPJ lookup autofill", () => { .send({ willSign: true }) .expect(404); }); + + it("sets and updates the legal representative fields, unlocked (unlike client_registrations' pj_rep_cpf)", async () => { + const id = await createReseller("qa-test- Revenda Representante"); + + const created = await agent(app) + .patch(`/api/v1/resellers/${id}`) + .set(...bearer(admin.accessToken)) + .send({ + repFullName: "qa-test- Representante Um", + repCpf: "52998224725", + repRole: "Diretor", + repEmail: "representante@example.com", + repMobilePhone: "11999990000", + repAuthorizationAck: true, + }) + .expect(200); + expect(created.body.rep_full_name).toBe("qa-test- Representante Um"); + expect(created.body.rep_cpf).toBe("52998224725"); + expect(created.body.rep_authorization_ack).toBe(true); + + const updated = await agent(app) + .patch(`/api/v1/resellers/${id}`) + .set(...bearer(admin.accessToken)) + .send({ repCpf: "11144477735" }) + .expect(200); + expect(updated.body.rep_cpf).toBe("11144477735"); + expect(updated.body.rep_full_name).toBe("qa-test- Representante Um"); // untouched field survives COALESCE + }); }); diff --git a/apps/core-web/src/lib/api/resellers.ts b/apps/core-web/src/lib/api/resellers.ts index 691fadc..aefa4db 100644 --- a/apps/core-web/src/lib/api/resellers.ts +++ b/apps/core-web/src/lib/api/resellers.ts @@ -39,6 +39,12 @@ export interface Reseller { contact_technical_name: string | null; contact_technical_email: string | null; contact_technical_phone: string | null; + rep_full_name: string | null; + rep_cpf: string | null; + rep_role: string | null; + rep_email: string | null; + rep_mobile_phone: string | null; + rep_authorization_ack: boolean; } /** Request body — camelCase, matches `CreateResellerDto` exactly. */ @@ -74,6 +80,13 @@ export interface CreateResellerInput { contactTechnicalName?: string; contactTechnicalEmail?: string; contactTechnicalPhone?: string; + + repFullName?: string; + repCpf?: string; + repRole?: string; + repEmail?: string; + repMobilePhone?: string; + repAuthorizationAck?: boolean; } /** Matches `UpdateResellerDto`: everything optional. */ diff --git a/apps/core-web/src/routes/resellers/ResellerFormPage.tsx b/apps/core-web/src/routes/resellers/ResellerFormPage.tsx index d8bc752..db3a0fa 100644 --- a/apps/core-web/src/routes/resellers/ResellerFormPage.tsx +++ b/apps/core-web/src/routes/resellers/ResellerFormPage.tsx @@ -23,7 +23,15 @@ import { ResellerPartnersSection } from "./ResellerPartnersSection.js"; * * Section order when editing (per product direction, same order as * `ClientRegistrationDetailPage`): Dados da revenda (incl. CNPJ lookup) -> - * Sócios -> Endereço -> Contatos -> Status. + * Representante legal -> Sócios -> Endereço -> Contatos -> Status. + * + * "Representante legal" (`rep_*`) mirrors `client_registrations.pj_rep_*` + * — added because reseller registration data will feed reseller contract + * creation later; a contract needs a named, authorized signer, not just + * the general Sócios list. Unlike `pj_rep_cpf` there, `repCpf` here is a + * plain editable field — see the migration + * `1789700100000_resellers-legal-representative.cjs` for why the same + * structural lock doesn't apply to resellers. */ interface FormState { name: string; @@ -39,6 +47,13 @@ interface FormState { cnpjSituation: string; mainActivity: string; + repFullName: string; + repCpf: string; + repRole: string; + repEmail: string; + repMobilePhone: string; + repAuthorizationAck: boolean; + addressZip: string; addressStreet: string; addressNumber: string; @@ -73,6 +88,13 @@ const EMPTY_FORM: FormState = { cnpjSituation: "", mainActivity: "", + repFullName: "", + repCpf: "", + repRole: "", + repEmail: "", + repMobilePhone: "", + repAuthorizationAck: false, + addressZip: "", addressStreet: "", addressNumber: "", @@ -112,6 +134,13 @@ function toFormState(reseller: Reseller): FormState { cnpjSituation: reseller.cnpj_situation ?? "", mainActivity: reseller.main_activity ?? "", + 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, + addressZip: reseller.address_zip ?? "", addressStreet: reseller.address_street ?? "", addressNumber: reseller.address_number ?? "", @@ -206,6 +235,13 @@ export function ResellerFormPage() { cnpjSituation: toOptional(form.cnpjSituation), mainActivity: toOptional(form.mainActivity), + repFullName: toOptional(form.repFullName), + repCpf: toOptional(form.repCpf), + repRole: toOptional(form.repRole), + repEmail: toOptional(form.repEmail), + repMobilePhone: toOptional(form.repMobilePhone), + repAuthorizationAck: form.repAuthorizationAck, + addressZip: toOptional(form.addressZip), addressStreet: toOptional(form.addressStreet), addressNumber: toOptional(form.addressNumber), @@ -418,6 +454,43 @@ export function ResellerFormPage() { ) : null} + + {isEditing ? ( + +

+ Quem assina pela revenda — usado depois na confecção de contrato com a revenda. +

+
+ updateField("repFullName", e.target.value)} + /> + updateField("repCpf", e.target.value)} /> + updateField("repRole", e.target.value)} /> + updateField("repEmail", e.target.value)} + /> + updateField("repMobilePhone", e.target.value)} + /> + +
+
+ ) : null} {isEditing && id ? ( diff --git a/docs/progress.md b/docs/progress.md index 7ba8f8e..33ae6d5 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -775,3 +775,21 @@ Data: 2026-09-18. Frontend da leva anterior (agente `eden-frontend`), construíd ### Verificação `pnpm turbo run typecheck build` 28/28 (antes e depois da correção). Suíte 106/106 (inalterada, mudança de frontend). Testado ao vivo em navegador real via Playwright, incluindo o cenário exato do bug (digitar 14 dígitos antes de qualquer busca — botão permanece "Buscar CNPJ" corretamente agora) e o ciclo completo com CNPJ real (Petrobras, nome fantasia e sócio confirmados). Revendas de teste desativadas ao final. + +--- + +## Representante legal da Revenda — preparação pra contrato com revenda + +Data: 2026-09-18. Usuário: "o cadastro da revenda tem que ser completo pq depois vamos usar esses dados para fazer o contrato com a revenda tb por dentro do erp". Implementado diretamente (mudança pequena e bem entendida, mesmo padrão já estabelecido). + +Investigando antes de mudar: o módulo de Assinatura Eletrônica já aceita `resellerId` como origem de um envelope (`CreateEnvelopeDto`, alternativa a `clientRegistrationId`), mas os `contratante.signers` de um envelope hoje vêm sempre digitados manualmente no corpo da requisição — não existe ainda nenhuma derivação automática a partir de `client_registration_partners`/`reseller_partners` (achado que vale registrar para quando essa integração for construída de verdade, não resolvido aqui — fora do pedido desta leva). O que faltava de concreto e imediato: `resellers`, ao contrário de `client_registrations`, não tinha um bloco de **representante legal** dedicado (`pj_rep_*` em client_registrations) — só a lista geral de sócios. Um contrato precisa de um signatário nomeado e autorizado, não só uma lista de sócios. + +Migration `packages/database/migrations/1789700100000_resellers-legal-representative.cjs`: `resellers` ganha `rep_full_name`, `rep_cpf`, `rep_role`, `rep_email`, `rep_mobile_phone`, `rep_authorization_ack` — mesmo shape de `client_registrations.pj_rep_*`. **Divergência deliberada**: lá `pj_rep_cpf` é estruturalmente bloqueado do PATCH genérico (só seria setável pelo fluxo de submissão pública original do legado, que o EDEN ainda não construiu — hoje é uma lacuna conhecida, o campo nunca é preenchido de fato). Revenda não tem esse fluxo de submissão pública (é sempre cadastro de backoffice), então não existe "momento de submissão original" pra travar atrás — `rep_cpf` aqui é um campo comum, livremente editável via `PATCH`, sem repetir uma trava que só faria sentido lá. + +Backend (`CreateResellerDto`, `resellers.service.ts`, repositório) e frontend (`ResellerFormPage.tsx`, novo card "Representante legal" entre "Dados da revenda" e "Sócios") seguindo o mesmo padrão já usado no resto do módulo. + +### Verificação +`pnpm db:migrate`/`down`/`migrate` de novo — limpo. `pnpm turbo run typecheck build` 28/28. Suíte 107/107 (106→107, 1 teste novo cobrindo set+update dos campos). Testado ao vivo em navegador real: ordem confirmada (`Dados da revenda → Representante legal → Sócios → Endereço → Contatos → Status`), Nome/CPF do representante preenchidos e confirmados persistentes após reload da página. Revenda de teste desativada ao final. + +### Pendências +A derivação automática de signatários de contrato a partir de `client_registration_partners`/`reseller_partners`/representante legal (pra realmente "usar na confecção do contrato") ainda não existe — hoje `CreateEnvelopeDto.signers` continua sendo preenchido manualmente independente da origem. Esse é o trabalho real que fecha o ciclo que o usuário descreveu; os dados já ficam prontos e completos esperando por ele. diff --git a/packages/database/migrations/1789700100000_resellers-legal-representative.cjs b/packages/database/migrations/1789700100000_resellers-legal-representative.cjs new file mode 100644 index 0000000..34c4fe0 --- /dev/null +++ b/packages/database/migrations/1789700100000_resellers-legal-representative.cjs @@ -0,0 +1,49 @@ +/** + * Representante legal da revenda — mirrors `client_registrations.pj_rep_*` + * (packages/database/migrations/1788447739229_client-registrations.cjs). + * Motivation (user, 2026-09-18): reseller registration data will later + * feed reseller contract creation inside the ERP, same as + * client_registrations already does for client contracts — a contract + * needs a named, authorized signer for the legal entity, not just the + * general Sócios list (`reseller_partners`, added in + * 1789700000000_resellers-expansion.cjs). + * + * Deliberate deviation from `client_registrations.pj_rep_cpf`: there it's + * structurally excluded from the generic admin PATCH (`UpdateClientRegistrationDto`) + * because in the legacy it's only ever set during the public self-service + * submission flow, which EDEN hasn't built yet — so it stays permanently + * unset there today, a known pre-existing gap, not something to replicate. + * Resellers have no such public submission flow (always backoffice-created) + * and no "original submission" moment to lock behind, so `rep_cpf` here is + * a plain, freely-PATCH-able column like every other reseller field — see + * `resellers.service.ts`, no new DTO exclusion added for it. + */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.sql(` + ALTER TABLE resellers + ADD COLUMN rep_full_name TEXT, + ADD COLUMN rep_cpf TEXT, + ADD COLUMN rep_role TEXT, + ADD COLUMN rep_email TEXT, + ADD COLUMN rep_mobile_phone TEXT, + ADD COLUMN rep_authorization_ack BOOLEAN NOT NULL DEFAULT false; + + COMMENT ON COLUMN resellers.rep_authorization_ack IS + 'Confirmação de que o representante está autorizado a agir pela revenda — mesmo campo de intenção que client_registrations.pj_rep_authorization_ack, sem o mesmo bloqueio estrutural de edição (ver cabeçalho da migration).'; + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + ALTER TABLE resellers + DROP COLUMN IF EXISTS rep_full_name, + DROP COLUMN IF EXISTS rep_cpf, + DROP COLUMN IF EXISTS rep_role, + DROP COLUMN IF EXISTS rep_email, + DROP COLUMN IF EXISTS rep_mobile_phone, + DROP COLUMN IF EXISTS rep_authorization_ack; + `); +}; diff --git a/packages/database/src/repositories/resellers.ts b/packages/database/src/repositories/resellers.ts index 530cca4..4a5ea81 100644 --- a/packages/database/src/repositories/resellers.ts +++ b/packages/database/src/repositories/resellers.ts @@ -33,6 +33,12 @@ export interface Reseller { contact_technical_name: string | null; contact_technical_email: string | null; contact_technical_phone: string | null; + rep_full_name: string | null; + rep_cpf: string | null; + rep_role: string | null; + rep_email: string | null; + rep_mobile_phone: string | null; + rep_authorization_ack: boolean; } export async function listResellers(includeInactive = false): Promise { @@ -79,6 +85,12 @@ export interface CreateResellerInput { contactTechnicalName?: string | null; contactTechnicalEmail?: string | null; contactTechnicalPhone?: string | null; + repFullName?: string | null; + repCpf?: string | null; + repRole?: string | null; + repEmail?: string | null; + repMobilePhone?: string | null; + repAuthorizationAck?: boolean; } export async function createReseller(input: CreateResellerInput): Promise { @@ -91,7 +103,8 @@ export async function createReseller(input: CreateResellerInput): Promise