• Arturo Alamilla
  • NEWBIE
  • 20 Points
  • Member since 2020
  • CRM Leader
  • Yuhu


  • Chatter
    Feed
  • 0
    Best Answers
  • 0
    Likes Received
  • 0
    Likes Given
  • 2
    Questions
  • 2
    Replies
I'm going to show to my users a Screen flow where they can create a case with a PDF related to the case using the "File upload" flow component.
When the case is created i'm going to use an Invocable action to send the case information like caseNumber, caseReason and the related PDF file to an endpoint of an external system (django framework).
This is what im doing:
 
public with sharing class CrearComentarioVM {
    
    @invocableMethod(label='Crear Comentario en VM')
    public static list<ResultadoEndpoint> crearComentario(list<DatosRecibidosFlow> comentarioCreado) {
        
        //find the file
        ContentVersion cv = [SELECT VersionData
            FROM ContentVersion 
            WHERE Id =: comentarioCreado[0].contentVersion
            WITH SECURITY_ENFORCED 
            LIMIT 1];

        blob pdfBlob = cv.VersionData;
        String encode64 = EncodingUtil.base64Encode(pdfBlob);
        
        //request body
        BodyJson body = new BodyJson();
        for(DatosRecibidosFlow d : comentarioCreado){
            body.archivo = encode64;
            body.ticket_salesforce_id = d.ticket_salesforce_id;
            body.accion = d.accion;
            body.descripcion = d.descripcion;
            body.salesforce_id = d.salesforce_id;
        }

        //request
        HttpRequest request = new HttpRequest();
        request.setMethod('POST');
        request.setEndpoint('callout:NamedVM/tickets/api/crear-seguimiento/');
        request.setHeader('Accept','application/json');
        request.setHeader('Content-type','application/json;charset=UTF-8');
        request.setBody(JSON.serialize(body));

        System.debug(request);
        System.debug(request.getBody());

        
        //send request
        Http http = new Http();
        HttpResponse response = http.send(request);
    
        //handle response
        if (response.getStatusCode() == 201) {
            System.debug('Response CodeStatus: ' + response.getStatusCode());
            String responseBody = response.getBody();
            RespuestaCrearComentarioVM ticket = RespuestaCrearComentarioVM.parse(responseBody);
            ResultadoEndpoint r = new ResultadoEndpoint();
            r.idVM = ticket.vm_id;
            r.responseCode = response.getStatusCode();
            list<ResultadoEndpoint> regresarRespuesta = new list<ResultadoEndpoint>();
            regresarRespuesta.add(r);
            return regresarRespuesta;

        }else {
            System.debug('Response CodeStatus: ' + response.getStatusCode()+ ' response '+response.getStatus());
            resultadoEndpoint r = new resultadoEndpoint();
            r.responseCode = response.getStatusCode();
            r.idVM = 0;
            list<resultadoEndpoint> regresarRespuesta = new list<resultadoEndpoint>();
            regresarRespuesta.add(r);
            return regresarRespuesta;
        }
    }


    //inner class for JSON Body to VM
    public class BodyJson{
        public String ticket_salesforce_id;
        public String accion;
        public String descripcion;
        public String salesforce_id;
        public String archivo;
    }

    //inner class for handle VM response
    public class ResultadoEndpoint{
        @InvocableVariable(label='El ID Generado por el VM')
        public Integer idVM;
        @InvocableVariable(label='La respuesta del Endpoint')
        public Integer responseCode;
    }

    //inner class to handle input flow data
    public class DatosRecibidosFlow{
        @InvocableVariable(label='El ID SF ContentVersion')
        public Id contentVersion;
        @InvocableVariable(label='El ID SF del Caso relacionado')
        public String ticket_salesforce_id;
        @InvocableVariable(label='El valor de AccionVM')
        public String accion;
        @InvocableVariable(label='El comentario escrito')
        public String descripcion;
        @InvocableVariable(label='El ID SF del Comentario creado')
        public String salesforce_id;
    }
}

Any thoughts?
Hello everybody.

I'm a new salesforce developer, and i'm facin my first real world problem in my company.

We have a custom object named "Solicitudes__c", is the detail side of a master relationship of another custom object.

I was asked to calculate the time between stages (Picklist field named "Etapas_de_solicitud__c" with 10 values).
My solution is use History object records to calculate the first date to enter one stage, and the last day touching that same stage for every stage. I had to create some sample records and move them through all stages.

Later i came out with this apex class:
public with sharing class SolicitudesTiempos {

    public static void tiempos(List<Solicitudes__c> solicitudes) {
        
        //transform the List to Set of Ids
        set<id> solicitudesIds = new set<Id>();
        for(solicitudes__c s : solicitudes){
            solicitudesIds.add(s.id);
        }

        //list used to update the Solicitudes__c records lateer
        List<Solicitudes__c> solicitudesActualizar = new List<Solicitudes__c>();

        //gather all Solicitudes__History records.
        list<Solicitudes__History> historialBuscado = [SELECT ParentId, NewValue, OldValue, CreatedDate
                                                        FROM Solicitudes__History 
                                                        WHERE ParentId =: solicitudesIds
                                                        AND Field = 'Etapas_de_Solicitud__c'
                                                        WITH SECURITY_ENFORCED
                                                        ORDER BY CreatedDate ASC];
                                               
                                                        
        // lists to store the history changes, accordingly.
        list<Solicitudes__History> historialCotizacion = new list<Solicitudes__History>();
        list<Solicitudes__History> historialAceptacion = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPreanalisis = new list<Solicitudes__History>();
        list<Solicitudes__History> historialDocumentos = new list<Solicitudes__History>();
        list<Solicitudes__History> historialCarga = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPendienteAnalizar = new list<Solicitudes__History>();
        list<Solicitudes__History> historialComite = new list<Solicitudes__History>();
        list<Solicitudes__History> historialCondicionada = new list<Solicitudes__History>();
        list<Solicitudes__History> historialAutorizada = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPendientedeFirma = new list<Solicitudes__History>();


        // loop throughout all solicitudes__history to store the appropiate history record to a list.
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Cotización' || s.OldValue == 'Cotización'){
                historialCotizacion.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Aceptación' || s.OldValue == 'Aceptación'){
                historialAceptacion.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Preanálisis' || s.OldValue == 'Preanálisis'){
                historialPreanalisis.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Documentos' || s.OldValue == 'Documentos'){
                historialDocumentos.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Aviso a sebas' || s.OldValue == 'Aviso a sebas'){
                historialCarga.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'pendiente de analizar' || s.OldValue == 'pendiente de analizar'){
                historialPendienteAnalizar.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'comite' || s.OldValue == 'comite'){
                historialComite.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'condicionado' || s.OldValue == 'condicionado'){
                historialCondicionada.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'autorizada' || s.OldValue == 'autorizada'){
                historialAutorizada.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'pendiente de firma' || s.OldValue == 'pendiente de firma'){
                historialPendientedeFirma.add(s);
            }
        }

        // loop to calcule every Solicitudes__c record time on each stage.
        for(Id s : solicitudesIds){

            Decimal tiempoCotizacion = SolicitudesTiempos.calcularTiempo(historialCotizacion, s);
            Decimal tiempoAceptacion = SolicitudesTiempos.calcularTiempo(historialAceptacion, s);
            Decimal tiempoPreanalisis = SolicitudesTiempos.calcularTiempo(historialPreanalisis, s);
            Decimal tiempoDocumentos = SolicitudesTiempos.calcularTiempo(historialDocumentos, s);
            Decimal tiempoCarga = SolicitudesTiempos.calcularTiempo(historialCarga, s);
            Decimal tiempoPendienteAnalizar = SolicitudesTiempos.calcularTiempo(historialPendienteAnalizar, s);
            Decimal tiempoComite = SolicitudesTiempos.calcularTiempo(historialComite, s);
            Decimal tiempoCondicionado = SolicitudesTiempos.calcularTiempo(historialCondicionada, s);
            Decimal tiempoAutorizada = SolicitudesTiempos.calcularTiempo(historialAutorizada, s);
            Decimal tiempoPendienteFirma = SolicitudesTiempos.calcularTiempo(historialPendientedeFirma, s);

            //store each calculation to the record's field and add it to a collection to update later.
            Solicitudes__c solicitud = new Solicitudes__c(Id = s, 
                                                            Tiempo_cotizacion__c=tiempoCotizacion,
                                                            Tiempo_Aceptacion__c=tiempoAceptacion,
                                                            Tiempo_preanalisis__c=tiempoPreanalisis,
                                                            Tiempo_Documentos__c=tiempoDocumentos,
                                                            Tiempo_Carga__c=tiempoCarga,
                                                            Tiempo_Por_analizar__c=tiempoPendienteAnalizar,
                                                            Tiempo_Comite__c=tiempoComite,
                                                            Tiempo_Condicionada__c=tiempoCondicionado,
                                                            Tiempo_Autorizada__c=tiempoAutorizada,
                                                            Tiempo_Pendiente_Firma__c=tiempoPendienteFirma);
            
            //add the new record to a collection to update later.
            solicitudesActualizar.add(solicitud);
        }
        
        //update the records
        update solicitudesActualizar;
    }


    //method to calculate time between stages.
    private static Decimal calcularTiempo (list<Solicitudes__History> historial, Id solicitudId){

        if(historial.size()>0 && historial != null){
            dateTime date1;
            dateTime date2;
            Decimal minutos;
            for(Solicitudes__History hist : historial){
                if(hist.ParentId == solicitudId && date1 == null){
                    date1 = hist.CreatedDate;
                }else if(hist.ParentId == solicitudId && date1 != null){
                    date2 = hist.CreatedDate;
                }
            }
            if(date1 != null && date2 != null){
                Long firstDate = date1.getTime();
                Long finalDate = date2.getTime();
                Decimal segundos = (finalDate-firstDate)/1000;
                minutos = segundos/60;
                return minutos;
            }else{
                return 0.00;
            }
        }else{
            return 0.00;
        }   
    }
}

Then i came out with an batch apex class:
public class SolicitudesTiemposBatch implements Database.Batchable<SObject> {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        String query = 'SELECT Id FROM Solicitudes__c '+
                'WHERE CreatedDate >= 2022-01-01T00:00:00Z '+
                'ORDER BY CreatedDate ASC';
                return DataBase.getQueryLocator(query);
    }

    public void execute(Database.BatchableContext bc, List<Solicitudes__c> scope){
        SolicitudesTiempos.tiempos(scope);
    }

    public void finish(database.BatchableContext bc){
    }
}

And create a Test class:
@isTest
public class SolicitudesTiemposBatchTest {
    @isTest(SeeAllData=true)
        static void test(){

            Test.startTest();
            SolicitudesTiemposBatch solicitudesBatch = new SolicitudesTiemposBatch();
            Database.executeBatch(solicitudesBatch, 200);
            Test.stopTest();
            
            System.assertEquals(199,[SELECT count() FROM Solicitudes__C], '199 solicitudes');
        }
}
And i get enough coverage to deploy production.
Code coverage

The problem is that when i'm trying to validate in production i see this problem: "System.UnexpectedException: No more than one executeBatch can be called from within a test method. Please make sure the iterable returned from your start method matches the batch size, resulting in one executeBatch invocation.
Stack Trace: External entry point"

Any suggestions?
I'm going to show to my users a Screen flow where they can create a case with a PDF related to the case using the "File upload" flow component.
When the case is created i'm going to use an Invocable action to send the case information like caseNumber, caseReason and the related PDF file to an endpoint of an external system (django framework).
This is what im doing:
 
public with sharing class CrearComentarioVM {
    
    @invocableMethod(label='Crear Comentario en VM')
    public static list<ResultadoEndpoint> crearComentario(list<DatosRecibidosFlow> comentarioCreado) {
        
        //find the file
        ContentVersion cv = [SELECT VersionData
            FROM ContentVersion 
            WHERE Id =: comentarioCreado[0].contentVersion
            WITH SECURITY_ENFORCED 
            LIMIT 1];

        blob pdfBlob = cv.VersionData;
        String encode64 = EncodingUtil.base64Encode(pdfBlob);
        
        //request body
        BodyJson body = new BodyJson();
        for(DatosRecibidosFlow d : comentarioCreado){
            body.archivo = encode64;
            body.ticket_salesforce_id = d.ticket_salesforce_id;
            body.accion = d.accion;
            body.descripcion = d.descripcion;
            body.salesforce_id = d.salesforce_id;
        }

        //request
        HttpRequest request = new HttpRequest();
        request.setMethod('POST');
        request.setEndpoint('callout:NamedVM/tickets/api/crear-seguimiento/');
        request.setHeader('Accept','application/json');
        request.setHeader('Content-type','application/json;charset=UTF-8');
        request.setBody(JSON.serialize(body));

        System.debug(request);
        System.debug(request.getBody());

        
        //send request
        Http http = new Http();
        HttpResponse response = http.send(request);
    
        //handle response
        if (response.getStatusCode() == 201) {
            System.debug('Response CodeStatus: ' + response.getStatusCode());
            String responseBody = response.getBody();
            RespuestaCrearComentarioVM ticket = RespuestaCrearComentarioVM.parse(responseBody);
            ResultadoEndpoint r = new ResultadoEndpoint();
            r.idVM = ticket.vm_id;
            r.responseCode = response.getStatusCode();
            list<ResultadoEndpoint> regresarRespuesta = new list<ResultadoEndpoint>();
            regresarRespuesta.add(r);
            return regresarRespuesta;

        }else {
            System.debug('Response CodeStatus: ' + response.getStatusCode()+ ' response '+response.getStatus());
            resultadoEndpoint r = new resultadoEndpoint();
            r.responseCode = response.getStatusCode();
            r.idVM = 0;
            list<resultadoEndpoint> regresarRespuesta = new list<resultadoEndpoint>();
            regresarRespuesta.add(r);
            return regresarRespuesta;
        }
    }


    //inner class for JSON Body to VM
    public class BodyJson{
        public String ticket_salesforce_id;
        public String accion;
        public String descripcion;
        public String salesforce_id;
        public String archivo;
    }

    //inner class for handle VM response
    public class ResultadoEndpoint{
        @InvocableVariable(label='El ID Generado por el VM')
        public Integer idVM;
        @InvocableVariable(label='La respuesta del Endpoint')
        public Integer responseCode;
    }

    //inner class to handle input flow data
    public class DatosRecibidosFlow{
        @InvocableVariable(label='El ID SF ContentVersion')
        public Id contentVersion;
        @InvocableVariable(label='El ID SF del Caso relacionado')
        public String ticket_salesforce_id;
        @InvocableVariable(label='El valor de AccionVM')
        public String accion;
        @InvocableVariable(label='El comentario escrito')
        public String descripcion;
        @InvocableVariable(label='El ID SF del Comentario creado')
        public String salesforce_id;
    }
}

Any thoughts?
Hello everybody.

I'm a new salesforce developer, and i'm facin my first real world problem in my company.

We have a custom object named "Solicitudes__c", is the detail side of a master relationship of another custom object.

I was asked to calculate the time between stages (Picklist field named "Etapas_de_solicitud__c" with 10 values).
My solution is use History object records to calculate the first date to enter one stage, and the last day touching that same stage for every stage. I had to create some sample records and move them through all stages.

Later i came out with this apex class:
public with sharing class SolicitudesTiempos {

    public static void tiempos(List<Solicitudes__c> solicitudes) {
        
        //transform the List to Set of Ids
        set<id> solicitudesIds = new set<Id>();
        for(solicitudes__c s : solicitudes){
            solicitudesIds.add(s.id);
        }

        //list used to update the Solicitudes__c records lateer
        List<Solicitudes__c> solicitudesActualizar = new List<Solicitudes__c>();

        //gather all Solicitudes__History records.
        list<Solicitudes__History> historialBuscado = [SELECT ParentId, NewValue, OldValue, CreatedDate
                                                        FROM Solicitudes__History 
                                                        WHERE ParentId =: solicitudesIds
                                                        AND Field = 'Etapas_de_Solicitud__c'
                                                        WITH SECURITY_ENFORCED
                                                        ORDER BY CreatedDate ASC];
                                               
                                                        
        // lists to store the history changes, accordingly.
        list<Solicitudes__History> historialCotizacion = new list<Solicitudes__History>();
        list<Solicitudes__History> historialAceptacion = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPreanalisis = new list<Solicitudes__History>();
        list<Solicitudes__History> historialDocumentos = new list<Solicitudes__History>();
        list<Solicitudes__History> historialCarga = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPendienteAnalizar = new list<Solicitudes__History>();
        list<Solicitudes__History> historialComite = new list<Solicitudes__History>();
        list<Solicitudes__History> historialCondicionada = new list<Solicitudes__History>();
        list<Solicitudes__History> historialAutorizada = new list<Solicitudes__History>();
        list<Solicitudes__History> historialPendientedeFirma = new list<Solicitudes__History>();


        // loop throughout all solicitudes__history to store the appropiate history record to a list.
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Cotización' || s.OldValue == 'Cotización'){
                historialCotizacion.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Aceptación' || s.OldValue == 'Aceptación'){
                historialAceptacion.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Preanálisis' || s.OldValue == 'Preanálisis'){
                historialPreanalisis.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Documentos' || s.OldValue == 'Documentos'){
                historialDocumentos.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'Aviso a sebas' || s.OldValue == 'Aviso a sebas'){
                historialCarga.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'pendiente de analizar' || s.OldValue == 'pendiente de analizar'){
                historialPendienteAnalizar.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'comite' || s.OldValue == 'comite'){
                historialComite.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'condicionado' || s.OldValue == 'condicionado'){
                historialCondicionada.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'autorizada' || s.OldValue == 'autorizada'){
                historialAutorizada.add(s);
            }
        }
        for(Solicitudes__History s : historialBuscado){
            if(s.NewValue == 'pendiente de firma' || s.OldValue == 'pendiente de firma'){
                historialPendientedeFirma.add(s);
            }
        }

        // loop to calcule every Solicitudes__c record time on each stage.
        for(Id s : solicitudesIds){

            Decimal tiempoCotizacion = SolicitudesTiempos.calcularTiempo(historialCotizacion, s);
            Decimal tiempoAceptacion = SolicitudesTiempos.calcularTiempo(historialAceptacion, s);
            Decimal tiempoPreanalisis = SolicitudesTiempos.calcularTiempo(historialPreanalisis, s);
            Decimal tiempoDocumentos = SolicitudesTiempos.calcularTiempo(historialDocumentos, s);
            Decimal tiempoCarga = SolicitudesTiempos.calcularTiempo(historialCarga, s);
            Decimal tiempoPendienteAnalizar = SolicitudesTiempos.calcularTiempo(historialPendienteAnalizar, s);
            Decimal tiempoComite = SolicitudesTiempos.calcularTiempo(historialComite, s);
            Decimal tiempoCondicionado = SolicitudesTiempos.calcularTiempo(historialCondicionada, s);
            Decimal tiempoAutorizada = SolicitudesTiempos.calcularTiempo(historialAutorizada, s);
            Decimal tiempoPendienteFirma = SolicitudesTiempos.calcularTiempo(historialPendientedeFirma, s);

            //store each calculation to the record's field and add it to a collection to update later.
            Solicitudes__c solicitud = new Solicitudes__c(Id = s, 
                                                            Tiempo_cotizacion__c=tiempoCotizacion,
                                                            Tiempo_Aceptacion__c=tiempoAceptacion,
                                                            Tiempo_preanalisis__c=tiempoPreanalisis,
                                                            Tiempo_Documentos__c=tiempoDocumentos,
                                                            Tiempo_Carga__c=tiempoCarga,
                                                            Tiempo_Por_analizar__c=tiempoPendienteAnalizar,
                                                            Tiempo_Comite__c=tiempoComite,
                                                            Tiempo_Condicionada__c=tiempoCondicionado,
                                                            Tiempo_Autorizada__c=tiempoAutorizada,
                                                            Tiempo_Pendiente_Firma__c=tiempoPendienteFirma);
            
            //add the new record to a collection to update later.
            solicitudesActualizar.add(solicitud);
        }
        
        //update the records
        update solicitudesActualizar;
    }


    //method to calculate time between stages.
    private static Decimal calcularTiempo (list<Solicitudes__History> historial, Id solicitudId){

        if(historial.size()>0 && historial != null){
            dateTime date1;
            dateTime date2;
            Decimal minutos;
            for(Solicitudes__History hist : historial){
                if(hist.ParentId == solicitudId && date1 == null){
                    date1 = hist.CreatedDate;
                }else if(hist.ParentId == solicitudId && date1 != null){
                    date2 = hist.CreatedDate;
                }
            }
            if(date1 != null && date2 != null){
                Long firstDate = date1.getTime();
                Long finalDate = date2.getTime();
                Decimal segundos = (finalDate-firstDate)/1000;
                minutos = segundos/60;
                return minutos;
            }else{
                return 0.00;
            }
        }else{
            return 0.00;
        }   
    }
}

Then i came out with an batch apex class:
public class SolicitudesTiemposBatch implements Database.Batchable<SObject> {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        String query = 'SELECT Id FROM Solicitudes__c '+
                'WHERE CreatedDate >= 2022-01-01T00:00:00Z '+
                'ORDER BY CreatedDate ASC';
                return DataBase.getQueryLocator(query);
    }

    public void execute(Database.BatchableContext bc, List<Solicitudes__c> scope){
        SolicitudesTiempos.tiempos(scope);
    }

    public void finish(database.BatchableContext bc){
    }
}

And create a Test class:
@isTest
public class SolicitudesTiemposBatchTest {
    @isTest(SeeAllData=true)
        static void test(){

            Test.startTest();
            SolicitudesTiemposBatch solicitudesBatch = new SolicitudesTiemposBatch();
            Database.executeBatch(solicitudesBatch, 200);
            Test.stopTest();
            
            System.assertEquals(199,[SELECT count() FROM Solicitudes__C], '199 solicitudes');
        }
}
And i get enough coverage to deploy production.
Code coverage

The problem is that when i'm trying to validate in production i see this problem: "System.UnexpectedException: No more than one executeBatch can be called from within a test method. Please make sure the iterable returned from your start method matches the batch size, resulting in one executeBatch invocation.
Stack Trace: External entry point"

Any suggestions?