0006 — Funil Comercial (proposta + contrato + assinatura + provisão)
Status
Accepted · 2026-04-26 · Autor: Alexandre + Claude. Funde os ADRs 0006/0007/0008 do v1 (PDF generation + Assinafy + public approval).
Contexto
Funil é coração comercial da plataforma. No v1, três ADRs separados cobriam: geração de PDF, integração Assinafy, fluxo de aprovação pública. No v2, unificamos em um ADR fundacional dado que tudo é coeso.
Multi-consultancy adiciona nuance: cada consultor tem seu funil isolado; clínica direta tem funil simplificado.
Decisão
1. Estágios da máquina de estados
CREATE TABLE public.leads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), consultancy_id UUID REFERENCES public.consultancies(id), -- NULLABLE (lead direto) full_name TEXT NOT NULL, email TEXT NOT NULL, phone TEXT, cnpj TEXT, specialty TEXT, stage TEXT NOT NULL DEFAULT 'new' CHECK (stage IN ('new', 'form_sent', 'form_answered', 'proposal_sent', 'proposal_accepted', 'contract_sent', 'signed', 'provisioned', 'lost')), source TEXT, owner_user_id UUID, lost_reason TEXT, notes TEXT, score INT CHECK (score IS NULL OR score BETWEEN 0 AND 100), next_followup_at TIMESTAMPTZ, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), deleted_at TIMESTAMPTZ);Stages canônicas:
new → form_sent → form_answered → proposal_sent → proposal_accepted → contract_sent → signed → provisioned
(qualquer estágio) → lost (com lost_reason)Cada transição:
- Atualiza
lead.stage - Cria
lead_activitiesrow (audit + timeline) - Pode disparar email/notification
2. Onboarding (questionário)
onboarding_templates armazena questionários reutilizáveis (estrutura JSON). onboarding_submissions armazena respostas:
CREATE TABLE public.onboarding_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, consultancy_id UUID REFERENCES public.consultancies(id), -- template específico do consultor fields JSONB NOT NULL, -- engine de forms (ADR 0005) active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW());
CREATE TABLE public.onboarding_submissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_id UUID NOT NULL REFERENCES public.onboarding_templates(id), lead_id UUID REFERENCES public.leads(id), -- nullable (lead pode submeter antes de virar lead formal) answers JSONB NOT NULL, submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), source_ip INET, user_agent TEXT);Lead recebe link público /onboarding/:token (token único, expira) → preenche → submete via edge function submit-onboarding-answer (sem auth).
3. Proposta
CREATE TABLE public.proposals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_id UUID NOT NULL REFERENCES public.leads(id), consultancy_id UUID NOT NULL REFERENCES public.consultancies(id), status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sent', 'viewed', 'in_negotiation', 'accepted', 'rejected', 'expired')), -- Componentes do plano saas_plan_price_brl_cents INT NOT NULL, -- preço do plano (R$ 700 default) consulting_fee_brl_cents INT, -- valor da consultoria (decisão do consultor) consulting_installments INT, -- Trial trial_days INT DEFAULT 14, -- Texto e contexto custom_message TEXT, saas_features_summary TEXT, -- pré-filled consulting_scope_summary TEXT, -- preenchido pelo consultor -- Snapshot pré-negociação (imutável após accepted) snapshot_at_acceptance JSONB, questioning_message TEXT, -- "tenho dúvida antes de aceitar" questioning_at TIMESTAMPTZ, sent_at TIMESTAMPTZ, accepted_at TIMESTAMPTZ, rejected_at TIMESTAMPTZ, rejection_reason TEXT, expires_at TIMESTAMPTZ, public_token TEXT UNIQUE, -- pra rota /proposta/:token created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());Proposta tem rota pública /proposta/:token (sem auth). Lead vê detalhes, escolhe:
- ✅ “Aceitar proposta” →
accepted_atsetado, snapshot tirado, trigger gera contrato - ❓ “Tenho uma dúvida antes” →
questioning_messagesalvo, consultor responde (UX sutil — evita rejeição direta) - ❌ “Recusar” →
rejected_at+rejection_reason
4. Contrato
CREATE TABLE public.contracts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), proposal_id UUID NOT NULL REFERENCES public.proposals(id), consultancy_id UUID NOT NULL REFERENCES public.consultancies(id), status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'pdf_generated', 'sent_for_review', 'lead_approved', 'sent_for_signature', 'signed', 'cancelled', 'expired')), pdf_url TEXT, -- Supabase Storage signed URL signed_pdf_url TEXT, -- após Assinafy -- Snapshot dos termos proposal_snapshot JSONB NOT NULL, contratada_legal_data JSONB NOT NULL, -- "ClinicGestor LTDA" (consultoria atendendo) — Razão social + CNPJ + endereço contratante_legal_data JSONB NOT NULL, -- clínica -- Assinafy assinafy_document_id TEXT, assinafy_envelope_id TEXT, -- Aprovação pública (lead aprova antes de assinar) public_approval_token TEXT UNIQUE, approved_at TIMESTAMPTZ, approval_request_adjustments TEXT, -- Assinatura signed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());5. Geração de PDF de contrato
Edge function generate-contract-pdf:
- Recebe
contract_id - Carrega snapshot dos termos
- Renderiza template (HTML → PDF via Puppeteer ou similar)
- Upload pra Supabase Storage (
contract-pdfsbucket, privado) - Atualiza
contracts.pdf_url(signed URL com expiração) - Status:
draft→pdf_generated
Templates parametrizados por consultoria:
- Razão social (consultoria)
- CNPJ
- Endereço
- Logo (de
consultancies.branding) - Texto base (template padrão da plataforma)
- Cláusulas customizáveis pelo consultor (futuro F2)
Cláusulas obrigatórias fixas pela plataforma (não removíveis):
- Adesão ao Programa Acolhe (com 6 vagas/ano + crédito narrativo R$ 400)
- Cobrança e fallback (regra Ideia 1)
- Cancelamento bilateral (regra Ideia 1)
- LGPD e consent (regra Ideia 9)
6. Aprovação pública (rota /contrato/:token)
Lead recebe email com link público (sem auth). Vê PDF do contrato. Opções:
- ✅ “Aprovo o contrato” →
approved_at+ cria envelope no Assinafy → statussent_for_signature - 📝 “Solicitar ajuste” →
approval_request_adjustmentsregistra, consultor responde, gera novo PDF - ❌ “Não aprovo” →
cancelled
7. Integração Assinafy
Ver ADR 0007.
8. Provisão
Após contract.status = 'signed', edge function provision-clinic:
- Cria registro em
clinicscom:consultancy_id= consultoria (se attribution)attribution_type = 'consultancy_acquisition'(oumarketplace_acquisitionse origem marketplace)attribution_consultancy_idsetado (IMUTÁVEL)attribution_set_at = NOW()provisioned_at = NOW()social_program_status = 'active'(cláusula contratual)
- Cria
auth.userspraclinic_owner(viaauth.admin.createUser) - Cria
clinic_membercomrole_key = 'clinic_owner',status = 'invited' - Cria customer no Asaas DO CONSULTOR + subscription R$ 700/mês com trial de N dias (campo
proposal.trial_days) - Determina
platform_subscription_state:- Trial > 0 →
'trialing'(libera acesso direto) - Trial = 0 →
'pending_first_payment'(espera 1ª fatura)
- Trial > 0 →
- Determina
billing_via:- Padrão:
'consultancy_asaas'(consultor cobra clínica) - Plataforma direta (sem consultor):
'platform_direct'(raríssimo na F1)
- Padrão:
- Envia email de welcome com magic link → owner faz primeiro login → cria senha → trial começa
- Atualiza
lead.stage = 'provisioned'
9. Eventos do funil em audit
Cada transição em leads.stage, proposals.status, contracts.status cria linha em lead_activities:
CREATE TABLE public.lead_activities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_id UUID NOT NULL REFERENCES public.leads(id), activity_type TEXT NOT NULL CHECK (activity_type IN ( 'created', 'form_sent', 'form_answered', 'proposal_drafted', 'proposal_sent', 'proposal_viewed', 'proposal_questioned', 'proposal_accepted', 'proposal_rejected', 'contract_drafted', 'contract_pdf_generated', 'contract_sent_for_review', 'contract_lead_approved', 'contract_adjustment_requested', 'contract_sent_for_signature', 'contract_signed', 'provisioned', 'lost', 'note', 'email_sent', 'sms_sent', 'call' )), actor_user_id UUID, details JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW());CHECK constraint é whitelist viva (test db-check-drift valida).
Consequências
Positivas
- Funil unificado com máquina de estados clara
- Audit completo via
lead_activities - Snapshots imutáveis no aceite (proteção legal)
- Multi-consultancy native desde o dia 1
- Compliance: Programa Acolhe na cláusula desde primeiro contrato
Negativas
- 4 tabelas + edge function complexa (
provision-clinic) - Snapshot duplica dados (necessário pra imutabilidade)
Riscos
- Bug em
provision-cliniccria estado inconsistente entreclinics+clinic_members+auth.users+ Asaas- Mitigação: idempotência (chave:
contract_id); rollback em transação Postgres + compensação pra side-effects externos
- Mitigação: idempotência (chave:
- Stage transitions sem validação (forçar
proposal_accepted→provisioneddireto, pulando contrato)- Mitigação: edge function valida transições permitidas
Alternativas consideradas
1. Sem snapshot — buscar dados live no momento da assinatura
- Prós: simplicidade
- Contras: se preços mudam entre proposta e assinatura, lead pode reclamar. Snapshot protege legalmente.
- Rejeitado.
2. Stages como tabela em vez de enum
lead_stages table com nome + descrição.
- Prós: mais flexível
- Contras: stages são fundação da máquina de estados; mudar valor é refactoring grande de código. Enum + CHECK é suficiente.
- Rejeitado.
Atualizações de documentos
- AGENTS.md §inventário (Funil comercial)
docs/funil-comercial.md- ADRs 0007 (Assinafy)
Referências
- ADR 0007 (Assinafy)
- ADR 0008 (modelo financeiro)
- ADR 0014 (Programa Acolhe — cláusula contratual)