0011 — Cancelamento Bilateral, Fallback de Cobrança e Transferência de Consultor
Status
Accepted · 2026-04-26 · Autor: Alexandre + Claude.
Contexto
3 cenários relacionados que precisam workflow claro:
- Cancelamento de consultoria (clínica e consultor decidem encerrar relação)
- Cancelamento de plano (clínica decide sair da plataforma)
- Transferência de consultor (clínica decide trocar)
Sem workflow estruturado, viram disputa onde plataforma é forçada a arbitrar. ADR 0008 já cobriu fallback automático em caso de inadimplência; este ADR formaliza o resto.
Decisão
1. Cancelamento bilateral via ticket
CREATE TABLE public.cancellation_tickets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), scope TEXT NOT NULL CHECK (scope IN ( 'consultancy_engagement', -- cancelar consultoria, manter plano 'platform_subscription' -- cancelar plano (cascata: cancela consultoria também) )), clinic_id UUID NOT NULL REFERENCES public.clinics(id), consultancy_id UUID REFERENCES public.consultancies(id), opened_by TEXT NOT NULL CHECK (opened_by IN ('clinic', 'consultancy', 'platform')), opened_by_user_id UUID NOT NULL REFERENCES auth.users(id), opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), proposed_terms JSONB, -- multa, prazo, etc — texto livre + estrutura status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'negotiating', 'agreed', 'rejected', 'expired', 'effected', 'escalated_to_platform')), -- Confirmação dupla (cancelamento bilateral) agreed_by_clinic_user_id UUID REFERENCES auth.users(id), agreed_by_clinic_at TIMESTAMPTZ, agreed_by_consultancy_user_id UUID REFERENCES auth.users(id), agreed_by_consultancy_at TIMESTAMPTZ, agreement_password_proof TEXT, -- hash imutável da dupla confirmação -- Escalação escalated_to_platform_at TIMESTAMPTZ, escalation_reason TEXT, platform_intervention_notes TEXT, platform_resolution TEXT CHECK (platform_resolution IN ( 'agreed_via_mediation', 'split_decision', 'platform_arbitration', 'left_unresolved' )), effected_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());
-- Imutabilidade da prova após acordoCREATE OR REPLACE FUNCTION public.prevent_agreed_ticket_mutation()RETURNS TRIGGER LANGUAGE plpgsql AS $$BEGIN IF OLD.agreement_password_proof IS NOT NULL AND NEW.agreement_password_proof IS DISTINCT FROM OLD.agreement_password_proof THEN RAISE EXCEPTION 'agreement_password_proof é IMUTÁVEL após acordo (regra inviolável ADR 0011)'; END IF; RETURN NEW;END;$$;2. Workflow de cancelamento de consultoria (scope = 'consultancy_engagement')
Quem pode abrir: clínica OU consultor (qualquer um).
Estado inicial: clinic.current_engagement_status = 'active'
1. Clínica abre ticket via /clinic/configuracoes/cancelar-consultoria OU consultor abre via /consultancy/clinics/:id/cancellation → cancellation_tickets row created com status='open', opened_by='clinic|consultancy' → clinic.current_engagement_status = 'cancellation_in_progress' → Email pra outra parte: "Ticket de cancelamento aberto; proposta: {terms}"
2. Outra parte responde: → Aceita: status='agreed' precisa de DUPLA confirmação → Rejeita: status='rejected'; ticket fechado; engagement continua → Negocia: edita proposed_terms; status='negotiating'
3. Após "agreed" inicial, ambos confirmam com SENHA: → Edge function valida senha do user via auth.signInWithPassword → Atualiza agreed_by_clinic_user_id + agreed_by_clinic_at → Quando ambos confirmaram: agreement_password_proof = SHA256(clinic_proof + consultancy_proof + timestamp) → Trigger bloqueia mudança em proof → status='agreed' (transição final)
4. Edge function 'cancellation-ticket-effect' processa: → clinic.current_consultancy_id = NULL → clinic.current_engagement_status = 'no_consultancy' → Cria clinic_consultancy_engagements row com end_reason='mutual_agreement' → Acesso do consultor aos dados da clínica é REVOGADO → Email pra clínica: "Você está livre pra escolher novo consultor no marketplace" → status='effected'Important: o que NÃO muda no cancelamento de consultoria
clinic.attribution_consultancy_idpermanece (IMUTÁVEL — regra ADR 0001)- Se
attribution_type = 'consultancy_acquisition': consultor que captou continua recebendo 50% mesmo após cancelamento (regra inviolável). Razão: ele trouxe o cliente; recompensa permanente. Plataforma não compensa cliente novo (Y) que assumir — Y só ganha consultoria.
3. Workflow de cancelamento de plano (scope = 'platform_subscription')
Quem pode abrir: apenas clínica.
Cliente abre via /clinic/configuracoes/cancelar-plano→ cancellation_tickets row com scope='platform_subscription', opened_by='clinic'→ Aviso: "Cancelar plano também cancela consultoria automaticamente"→ Cliente confirma com senha→ Edge function 'cancellation-ticket-effect': → Cancela subscription Asaas (ambas — consultor e plataforma) → clinic.platform_subscription_state = 'cancelled' → clinic.current_engagement_status = 'no_consultancy' → 30 dias de janela read-only (cliente exporta dados — reverse migration) → Após 30d: read-only termina, mas dados ficam em cold storage 20 anos (CFM)→ status='effected'4. Transferência de consultor
Quando clínica órfã (cancelou consultor anterior) escolhe novo no marketplace:
1. Clínica navega /consultores2. Clica em consultor X3. "Tornar X minha consultoria"4. Edge function: → clinic.current_consultancy_id = X.id → clinic.current_engagement_status = 'active'
Se attribution_type já é 'marketplace_acquisition': → who_receives_commission(clinic_id) passa a retornar X.id → X recebe 50% a partir do próximo ciclo
Se attribution_type é 'consultancy_acquisition' (clínica veio via lead de outro consultor antes): → attribution permanece (consultor original continua recebendo 50%) → X recebe APENAS valor da consultoria que ele cobrar (não 50% do plano)
→ Cria clinic_consultancy_engagements row com new started_at → Email pra X: "Você tem nova clínica! Clínica Y solicita seu atendimento"5. Histórico imutável de engagements
CREATE TABLE public.clinic_consultancy_engagements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), clinic_id UUID NOT NULL REFERENCES public.clinics(id), consultancy_id UUID NOT NULL REFERENCES public.consultancies(id), started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ended_at TIMESTAMPTZ, end_reason TEXT CHECK (end_reason IN ( 'contract_completed_no_renewal', 'mutual_agreement', 'unilateral_clinic', 'unilateral_consultancy', 'consultancy_revoked_by_platform', 'clinic_cancelled_subscription' )), contract_terms_snapshot JSONB, -- snapshot dos termos no início cancellation_ticket_id UUID REFERENCES public.cancellation_tickets(id), notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW());Cada engagement (período em que consultor X atendeu clínica Y) é uma linha. Imutável após ended_at.
6. Escalação pra plataforma
Se ticket fica open ou negotiating por 14 dias sem acordo:
- Notificação automática pra clínica: “Você pode escalar pra plataforma intermediar”
- Se clínica escala:
escalated_to_platform_atsetado,escalation_reasoninformado - Alexandre vê em
/platform/cancellation-tickets - Alexandre intermedia (não decide mérito) — facilita comunicação
- Resolução:
platform_resolutioncom 4 opções (agreed_via_mediation,split_decision,platform_arbitration,left_unresolved) - Plataforma deixa CLARO em comunicação que não é responsável pelo trabalho do consultor — só intermedia contratos
7. Fallback de cobrança (cross-reference ADR 0008)
Já detalhado em ADR 0008 §6. Resumo:
- Cliente não paga (30d) → clínica suspensa, ninguém recebe
- Consultor não paga plataforma (30d) → fallback automático pra plataforma cobrar direto
Estes são fallbacks automáticos, não bilaterais. Não precisam de ticket.
8. Descredenciamento de consultor
Casos extremos onde plataforma decide revogar:
Cenário: consultor com 3+ disputas escaladas em 90 dias OU comportamento inaceitável (fraude detectada, fraude do Acolhe, abuso)
Workflow:1. Alexandre abre ticket scope='consultancy_engagement' opened_by='platform' pra cada clínica do consultor2. Notificação: "Aviso de descredenciamento; você tem 30 dias pra negociar saída"3. Após 30 dias: → consultancies.credentialed_status = 'revoked' → consultancies.credentialed_revoked_reason setado → Pra cada clínica do consultor: - Se attribution_type='consultancy_acquisition': comissão para imediatamente (consultor revogado perde direito) - clinic.current_consultancy_id = NULL - Email pra clínica: "Seu consultor foi descredenciado; escolha novo no marketplace ou continue como direta" → Comissão por 30 dias após revogação (proteção legal — proporcional ao trabalho já realizado)Consequências
Positivas
- Cancelamento previsível: ticket bilateral evita disputa unilateral
- Imutabilidade legal:
agreement_password_proofé prova jurídica - Plataforma neutra: intermedia mas não arbitra mérito
- Attribution preservada: quem captou ganha pra sempre (mesmo se cliente trocar de consultor)
- Reverse migration garantida: cliente que sai exporta dados em 30 dias
Negativas
- Workflow complexo (3 estados + escalação + dupla confirmação)
- Tickets podem virar burocracia se mal-usados
Riscos
- Cliente assina com senha mas reclama depois (“não fui eu”)
- Mitigação: dupla confirmação (clínica + consultor) + IP + user_agent + timestamp imutável
- Plataforma pressionada pra arbitrar mérito
- Mitigação: comunicação clara em todos os canais que plataforma “intermedia, não arbitra”
Alternativas consideradas
1. Cancelamento unilateral (qualquer parte cancela)
- Prós: simplicidade
- Contras: gera disputa de multa, gera fraude (clínica cancela sem avisar consultor; consultor reclama)
- Rejeitado.
2. Plataforma decide mérito
- Prós: resolve disputa
- Contras: vira árbitro de relações comerciais; carga jurídica enorme
- Rejeitado — plataforma intermedia, não decide.
3. Comissão zerada após cancelamento (mesmo pra acquisition)
- Prós: simplicidade
- Contras: penaliza captação injustamente; consultor que trouxe cliente perde tudo se cliente trocar
- Rejeitado — attribution permanente é princípio.
Atualizações de documentos
- AGENTS.md §5.7.3 (cancellation_tickets agreed imutáveis)
- ADR 0001 (referência cruzada — attribution imutável)
- ADR 0008 (fallback automático)
docs/clinic/configuracoes-cancelar-consultoria.mddocs/clinic/configuracoes-cancelar-plano.mddocs/edge-functions/cancellation-ticket-effect.md
Referências
- ADR 0001 (attribution imutável)
- ADR 0008 (fallback automático em inadimplência)
- ADR 0010 (marketplace + transferência)