InícioDesafios › Desafio 04

Desafio 04 Nível: Intermediário

Uma task por venda, mesmo com o trigger rodando duas vezes

Este é o bug que ninguém vê nascer. O trigger funciona, o teste passa, e semanas depois o time de pós-venda pergunta por que todo cliente novo tem duas tarefas de onboarding iguais. Ninguém mexeu no código: alguém criou um Flow que atualiza um campo da oportunidade, e isso faz o seu trigger rodar de novo na mesma transação. Duplicação em silêncio é o sintoma clássico de trigger sem controle de reexecução.

Trigger handler Trigger.oldMap Reexecução Variável estática Bulkificação DML

O cenário

Quando uma oportunidade é ganha, a operação quer uma tarefa de onboarding criada automaticamente para o dono do negócio. A primeira versão entregue foi esta, e ela tem três problemas graves:

trigger OpportunityTrigger on Opportunity (after update) {
    for (Opportunity opp : Trigger.new) {
        if (opp.StageName == 'Closed Won') {
            // NAO faca isso: DML dentro do laco
            insert new Task(
                WhatId = opp.Id,
                Subject = 'Iniciar onboarding do cliente',
                ActivityDate = Date.today().addDays(2)
            );
        }
    }
}

Primeiro: DML dentro do laço estoura o limite com lote grande. Segundo: a condição olha só o estágio atual, então qualquer edição numa oportunidade já ganha cria outra tarefa. Terceiro, o mais sorrateiro: quando um Flow ou workflow atualiza um campo da mesma oportunidade, o Salesforce executa o trigger de novo na mesma transação, e essa reexecução enxerga os valores antigos originais, como se a oportunidade tivesse acabado de ser ganha outra vez.

O que você deve construir

Um trigger fino que delega para uma classe handler chamada OnboardingHandler, com um método público e estático, exatamente com esta assinatura:

public static void processarGanhas(List<Opportunity> novas, Map<Id, Opportunity> antigas)

O método recebe Trigger.new e Trigger.oldMap e cria a tarefa de onboarding para cada oportunidade que entrou em Closed Won nesta transação.

Regras de aceite

  • Exatamente uma tarefa por oportunidade ganha, mesmo que o trigger execute mais de uma vez na mesma transação (reexecução por Flow ou workflow field update).
  • Só cria quando o estágio MUDOU para ganho. Editar uma oportunidade que já estava em Closed Won não cria nada. A comparação usa o valor anterior, não só o atual.
  • Um único DML por execução, não importa se o lote tem 1 ou 200 oportunidades. Nenhum DML dentro de laço.
  • O controle de reexecução é por registro, não por transação. Um update de 201 oportunidades roda o trigger em dois blocos na mesma transação; o seu controle não pode fazer o segundo bloco ser ignorado. Um simples Boolean estático reprova aqui.
  • Lista sem nenhuma oportunidade recém-ganha: nenhum DML executado e nenhuma exceção.

O que este desafio avalia

  • Se você entende por que um trigger roda mais de uma vez na mesma transação, e o detalhe de que a reexecução enxerga os valores antigos originais.
  • Se você compara com Trigger.oldMap em vez de olhar só o estado atual.
  • Se o seu controle de reexecução sobrevive a lotes com mais de 200 registros.
  • Se você coleta primeiro e faz um DML só no fim.
Travei. Me dá uma direção (sem entregar a resposta)

Três perguntas que destravam:

  • O que uma variável static guarda durante uma transação no Apex, e o que acontece com ela quando a transação termina?
  • Se um Boolean estático vira true no primeiro bloco de 200 registros, o que acontece com os registros do segundo bloco da mesma transação?
  • Que estrutura guarda "quais registros eu já tratei" em vez de "eu já rodei"?

Termos de busca que ajudam: apex trigger fires twice workflow field update, apex trigger recursion static variable, trigger oldmap detect field change.

Como testar

Cole no Execute Anonymous. O teste força um lote de 201 (dois blocos de trigger na mesma transação), depois simula a reexecução do jeito que a automação faz: chamando o handler de novo com os valores antigos originais.

// 1) Lote de 201: forca DOIS blocos de trigger na mesma transacao
List<Opportunity> opps = new List<Opportunity>();
for (Integer i = 0; i < 201; i++) {
    opps.add(new Opportunity(
        Name = 'Desafio04-' + i,
        StageName = 'Prospecting',
        CloseDate = Date.today().addDays(30)
    ));
}
insert opps;

for (Opportunity o : opps) { o.StageName = 'Closed Won'; }
update opps; // dispara o trigger real (201 = blocos de 200 + 1)

// 2) Simula a reexecucao da automacao: os MESMOS registros, com os valores
//    antigos ORIGINAIS (e assim que o Salesforce reexecuta apos field update)
Map<Id, Opportunity> antigasOriginais = new Map<Id, Opportunity>();
for (Opportunity o : opps) {
    antigasOriginais.put(o.Id, new Opportunity(Id = o.Id, StageName = 'Prospecting'));
}
List<Opportunity> atuais = [SELECT Id, StageName, OwnerId FROM Opportunity WHERE Id IN :opps];
OnboardingHandler.processarGanhas(atuais, antigasOriginais);

// 3) Provas
Integer tarefas = [SELECT count() FROM Task
                   WHERE WhatId IN :opps AND Subject = 'Iniciar onboarding do cliente'];
System.assertEquals(201, tarefas, 'esperava exatamente UMA task por oportunidade ganha');
System.debug('OK: 201 oportunidades, 201 tarefas, zero duplicada.');

Gabarito comentado

A solução completa destrinchada: por que a reexecução engana a comparação com o valor antigo, por que o Boolean estático quebra em lote grande, e onde o Set de Ids entra. Tente resolver antes de abrir.

Ver o gabarito

Resolveu? O que fica não é o Set de Ids: é o hábito de perguntar, para todo trigger que você escrever, "o que acontece se isto rodar duas vezes na mesma transação?". Em org com automação de verdade, a pergunta não é se vai reexecutar, é quando.