feat(api): Quotes, Client Registration e esqueleto de Contratos

Fecha o núcleo comercial da Fase 2/3: motor de precificação de Ofertas
(feito diretamente, dado o risco de erro de arredondamento/regra de
negócio), Cadastro de Cliente e esqueleto de Contratos (via 2 subagentes
em paralelo, salvos manualmente após ambos baterem no limite de gasto
mensal da conta — trabalho não commitado nos worktrees foi revisado
arquivo a arquivo e copiado para main, com as peças faltantes
completadas diretamente).

- Quotes: pricing.ts porta as fórmulas do legado (eden.md §7/§8) —
  needsApproval, faixas de condição de pagamento, efeito legal da
  fidelidade condicionado a approval_status==='approved'. Fluxo
  criar→decidir aprovação→fechar negócio (vinculado a um cadastro de
  cliente, com guarda contra fechamento duplo).
- Client Registration: cadastro direto (eden.md §3), MVP autenticado-only
  (sem token público de autoatendimento, upload ou lookup CNPJ/CEP nesta
  fase — documentado em comentário).
- Contracts: máquina de estados do ADR-0006 imposta via SELECT...FOR
  UPDATE + tabela de transições, com signed_at/activated_at e histórico
  de status; 4 das 11 tabelas do ADR-0006 (as demais adiadas).

2 bugs próprios corrigidos antes do commit: UpdateQuoteInput usava
COALESCE para campos financeiros (undefined do JS apagaria valor em
update parcial) — campos tornados obrigatórios; User.reseller_id/
can_approve_discount faltavam em types.ts apesar das colunas já
existirem no banco desde migrations anteriores.

Verificado: pnpm turbo run typecheck build (28/28), pnpm test (15/15),
e todo o fluxo testado ao vivo via curl contra a stack Docker real,
incluindo cenários de erro (transição de status inválida, FK de produto
inexistente, fechamento duplo, 401 sem token) e trilha de auditoria
conferida no Postgres.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 12:25:49 -03:00
parent e8014b4135
commit 18838a9031
36 changed files with 3199 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
/**
* Customer 360 / Client Registration — eden.md §3 §1.1-1.2. Authenticated-
* only MVP (Master Prompt §6.5): deliberately out of scope for this round
* — the public unauthenticated token-based self-service form
* (`GET/POST /public-client-registration/:token`), file attachment
* upload/storage, CNPJ/CEP external lookup integration, the "official"
* `cnpj_address_*` Receita Federal address block, and the
* `*_externally_verified`/`*_verification_*` columns (only the format-
* check booleans `cpf_format_valid`/`cnpj_format_valid` are kept). Those
* all land in a later phase.
*
* `quote_id` is a plain nullable UUID WITHOUT a foreign key — the `quotes`
* table was built in a parallel migration; the FK linking the two is
* added in a follow-up migration once both are confirmed present (mirrors
* the legacy's own history: client_registration_id was itself added to
* quotes in a later migration, 1785513008756).
*/
exports.shorthands = undefined;
exports.up = (pgm) => {
pgm.sql(`
CREATE SEQUENCE client_registrations_client_code_seq START 1;
CREATE TABLE client_registrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID REFERENCES users(id),
quote_id UUID,
reseller_id UUID REFERENCES resellers(id),
token TEXT NOT NULL UNIQUE,
token_created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
client_code INTEGER NOT NULL UNIQUE DEFAULT nextval('client_registrations_client_code_seq'),
is_portability BOOLEAN NOT NULL DEFAULT false,
person_type TEXT NOT NULL CHECK (person_type IN ('pf','pj')),
registration_status TEXT NOT NULL DEFAULT 'rascunho' CHECK (registration_status IN ('rascunho','pendente_validacao','ativo','bloqueado','inativo')),
submitted_at TIMESTAMPTZ,
activated_at TIMESTAMPTZ,
internal_notes TEXT,
address_zip TEXT,
address_street TEXT,
address_number TEXT,
address_complement TEXT,
address_neighborhood TEXT,
address_city TEXT,
address_state TEXT,
address_country TEXT NOT NULL DEFAULT 'Brasil',
contact_principal_name TEXT,
contact_principal_email TEXT,
contact_principal_phone TEXT,
contact_financial_name TEXT,
contact_financial_email TEXT,
contact_financial_phone TEXT,
contact_technical_name TEXT,
contact_technical_email TEXT,
contact_technical_phone TEXT,
pf_full_name TEXT,
pf_cpf TEXT,
pf_birth_date DATE,
pf_email TEXT,
pf_mobile_phone TEXT,
pf_alt_phone TEXT,
pf_social_name TEXT,
pf_id_document TEXT,
pf_id_issuer TEXT,
pj_cnpj TEXT,
pj_company_name TEXT,
pj_trade_name TEXT,
pj_state_registration TEXT,
pj_municipal_registration TEXT,
pj_opening_date DATE,
pj_legal_nature TEXT,
pj_cnpj_situation TEXT,
pj_main_activity TEXT,
pj_rep_full_name TEXT,
pj_rep_cpf TEXT,
pj_rep_role TEXT,
pj_rep_email TEXT,
pj_rep_mobile_phone TEXT,
pj_rep_authorization_ack BOOLEAN NOT NULL DEFAULT false,
cpf_format_valid BOOLEAN,
cnpj_format_valid BOOLEAN,
portability_numbers JSONB NOT NULL DEFAULT '[]'::jsonb,
portability_ranges JSONB NOT NULL DEFAULT '[]'::jsonb,
ixc_client_id TEXT,
ixc_contract_number TEXT
);
COMMENT ON TABLE client_registrations IS
'Customer 360 / cadastro de cliente PF/PJ — eden.md §3. MVP autenticado apenas (sem fluxo público por token, sem anexos, sem lookup externo de CNPJ/CEP) — ver comentário no topo desta migration.';
COMMENT ON COLUMN client_registrations.quote_id IS
'Referencia quotes(id) sem FK por enquanto — FK a ser adicionada em migration futura.';
COMMENT ON COLUMN client_registrations.pf_cpf IS
'Imutável fora do fluxo de submissão original — nunca exposto em rota de edição administrativa (eden.md §1.1.2). Mesma regra para pj_cnpj e pj_rep_cpf.';
CREATE TRIGGER trg_client_registrations_updated_at BEFORE UPDATE ON client_registrations
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE INDEX idx_client_registrations_status ON client_registrations(registration_status);
CREATE INDEX idx_client_registrations_reseller ON client_registrations(reseller_id);
CREATE INDEX idx_client_registrations_created_by ON client_registrations(created_by);
CREATE TABLE client_registration_partners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_registration_id UUID NOT NULL REFERENCES client_registrations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
document TEXT,
qualification TEXT,
source TEXT NOT NULL DEFAULT 'manual',
cpf TEXT,
will_sign BOOLEAN NOT NULL DEFAULT false,
signature_email TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE client_registration_partners IS
'Sócios/QSA — eden.md §1.2, §4. source default "manual" (não "brasilapi" como no legado) — sem integração de lookup de CNPJ nesta fase.';
CREATE INDEX idx_client_registration_partners_registration ON client_registration_partners(client_registration_id);
`);
};
exports.down = (pgm) => {
pgm.sql(`
DROP TABLE IF EXISTS client_registration_partners;
DROP TABLE IF EXISTS client_registrations;
DROP SEQUENCE IF EXISTS client_registrations_client_code_seq;
`);
};