Plano — transferência de cliente por organização

TLDR: novo service transacional + job + ação no ActiveAdmin, movendo OrganizationCustomer e Payment apenas das organizações escolhidas, sem desativar nenhum cliente.

Spec correspondente: ../specs/20260407150300_organization_transfer.md.

Fases

1. Semântica e contrato de UX

  • [x] Nome da feature: transferência por organização (não “merge total”)
  • [x] Regras fechadas: mantém source e target ativos; não altera email; atualiza Asaas somente para as organizações selecionadas
  • [x] Operador escolhe cliente de origem por email e cliente de destino por email
  • [x] Resolver emails para IDs somente no backend, com validação final
  • [x] Bloquear a operação quando source == target

2. Service

  • [x] Criar app/services/organization_customer_transfer_service.rb
  • [x] Interface: initialize(source_customer_id:, target_customer_id:, organization_ids:, run_other_services: false)
  • [x] Método principal process, envolvido em transação

Validações de entrada:

  • [x] source_customer_id e target_customer_id existentes
  • [x] source != target
  • [x] organization_ids presente e não vazio
  • [x] Source precisa ter OrganizationCustomer nas organizações informadas
  • [x] Normalizar organization_ids (inteiros únicos)
  • [x] Rejeitar organizações que não pertencem ao source
  • [x] Tratar email ambíguo na resolução — bloquear e pedir seleção explícita

Seleção de vínculos no escopo:

  • [x] Buscar apenas OrganizationCustomer do source para organization_ids
  • [x] Guardar a coleção para o log final
  • [x] A lista de organizações da UX vem apenas das elegíveis do source, não da lista global
  • [x] Sem vínculos elegíveis, não permitir confirmar

Migração dentro do escopo:

  • [x] Para cada vínculo selecionado, find_or_initialize_by(organization_id:, customer_id: target.id, gateway_customer_id:) e salvar
  • [x] Política de conflito idempotente — não falhar se já existir igual
  • [x] Regra definida para conflito de gateway_customer_id no destino
  • [x] Remover do source apenas os vínculos transferidos, sem tocar nas organizações fora de organization_ids
  • [x] Migrar pagamentos: Payment com customer_id = source.id, joins(:checkout) filtrando checkouts.organization_id IN organization_ids, e update_all(customer_id: target.id) só nesse conjunto

Garantias negativas:

  • [x] Não chamar nada equivalente a update_wrong_customer_as_inactive
  • [x] Garantir no código que active de source/target não é alterado
  • [x] Não chamar IbftEmailSyncJob neste fluxo
  • [x] Fluxo dedicado de sync do Asaas por organization_ids, sem sincronização global

3. Auditoria

  • [x] WebhookLog com event_type novo, ex.: organization_customer_transfer
  • [x] Payload mínimo: source_customer_id, target_customer_id, source_email, target_email, organization_ids, moved_gateway_customer_ids, moved_payments_count, performed_by_user_id

4. Job

  • [x] Criar app/jobs/organization_customer_transfer_job.rb
  • [x] perform(source_customer_id, target_customer_id, organization_ids, run_other_services: false), delegando ao service

5. ActiveAdmin

  • [x] Fluxo dedicado “Transferir orgs” em app/admin/customers.rb
  • [x] Campos: email de origem, email de destino, organizações elegíveis (multi-select)
  • [x] Novo member_action (POST) recebendo source_email, target_email, organization_ids (csv ou array)
  • [x] Enfileirar OrganizationCustomerTransferJob

UX mínima:

  • [x] Prévia de impacto antes de confirmar — quantos OrganizationCustomer e quantos Payment serão movidos por organização
  • [x] Mensagens de confirmação: “Somente orgs selecionadas serão transferidas”, “Nenhum cliente será desativado”, “E-mails não serão alterados”
  • [x] Retorno com notice/alert apropriado

6. Concorrência

  • [x] Evitar processamento simultâneo para o mesmo source_customer_id
  • [x] Lock no fluxo transacional
  • [x] Comportamento consistente em retries de job

7. Testes

Service:

  • [x] Cenário feliz — move OrganizationCustomer e Payment apenas das organizações selecionadas
  • [x] Mantém o source ativo
  • [x] Não chama IbftEmailSyncJob
  • [x] Idempotência — rodar duas vezes não duplica vínculo
  • [x] Erro quando o source não tem vínculo nas organizações pedidas
  • [x] Erro quando a organização selecionada não é elegível no source
  • [x] Conflito de gateway_customer_id no destino
  • [x] Concorrência — duas transferências simultâneas
  • [x] Atualiza o Asaas somente das organizações selecionadas, e não das que estão fora do escopo

Job:

  • [x] Garante a delegação correta de argumentos para o service

Admin:

  • [x] member_action enfileira o job com os params corretos
  • [x] Params inválidos respondem com feedback
  • [x] Email inexistente (origem ou destino)
  • [x] Email ambíguo
  • [x] Prévia vazia — nada para transferir

8. Documentação e rollout

  • [x] Documentar quando usar merge total vs. transferência por organização, com o exemplo prático e o checklist pré-operação
  • [x] Liberar para poucos operadores/admins primeiro
  • [x] Monitorar o WebhookLog do novo evento por alguns dias
  • [x] Ajustar mensagens e validações conforme uso real

Verificação

  • bundle exec rubocop — obrigatório após qualquer mudança em Ruby
  • Rodar as specs afetadas (service, job, admin)