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:
@@ -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;
|
||||
`);
|
||||
};
|
||||
Reference in New Issue
Block a user