Plano — transferência de cliente por organização
TLDR: novo service transacional + job + ação no ActiveAdmin, movendo
OrganizationCustomerePaymentapenas 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
sourceetargetativos; 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_idetarget_customer_idexistentes - [x]
source != target - [x]
organization_idspresente e não vazio - [x] Source precisa ter
OrganizationCustomernas 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
OrganizationCustomerdo source paraorganization_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_idno destino - [x] Remover do source apenas os vínculos transferidos, sem tocar nas organizações fora de
organization_ids - [x] Migrar pagamentos:
Paymentcomcustomer_id = source.id,joins(:checkout)filtrandocheckouts.organization_id IN organization_ids, eupdate_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
activede source/target não é alterado - [x] Não chamar
IbftEmailSyncJobneste fluxo - [x] Fluxo dedicado de sync do Asaas por
organization_ids, sem sincronização global
3. Auditoria
- [x]
WebhookLogcomevent_typenovo, 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) recebendosource_email,target_email,organization_ids(csv ou array) - [x] Enfileirar
OrganizationCustomerTransferJob
UX mínima:
- [x] Prévia de impacto antes de confirmar — quantos
OrganizationCustomere quantosPaymentserã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/alertapropriado
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
OrganizationCustomerePaymentapenas 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_idno 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_actionenfileira 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
WebhookLogdo 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)