Pular para o conteúdo

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:

  1. Cancelamento de consultoria (clínica e consultor decidem encerrar relação)
  2. Cancelamento de plano (clínica decide sair da plataforma)
  3. 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 acordo
CREATE 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_id permanece (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 /consultores
2. Clica em consultor X
3. "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_at setado, escalation_reason informado
  • Alexandre vê em /platform/cancellation-tickets
  • Alexandre intermedia (não decide mérito) — facilita comunicação
  • Resolução: platform_resolution com 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 consultor
2. 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.md
  • docs/clinic/configuracoes-cancelar-plano.md
  • docs/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)