Guide voice management

LE GUIDE UTILISATEUR DE VOTRE SOLUTION CALL CENTER

Extension du comportement du connecteur

1 - Extension du comportement du connecteur

Depuis la version 2.0 il est possible de faire du travail avant, après ou à la place de la recherche du numéro de l’appelant.

Il est possible de créer un Contact en amont si vous en avez besoin avant de le rechercher par notre méthode par exemple.
Il est aussi possible de créer ou rechercher un Case et de retourner cet Id au lieu de notre recherche.
Ou alors de complètement revoir vous même l’intégralité de la recherche selon vos propres besoins ex : recherche différente en fonction du profil de l’opérateur.

2 - Activer mes redéfinitions

Créer une classe qui hérite de AxiaVM.Extension :

				
					global class MyAxialysExtension extends AxiaVM.Extension {
    // code d'extension ici
}



				
			

Pour utiliser cette extension de code il faut insérer dans la table AxiaVM__ConfigCTI__c un élément avec

Name = 'redefined_class'et AxiaVM__value__c = 'MyAxialysExtension'

ou

AxiaVM__value__c = 'MyOrgNamespace.MyAxialysExtension'

si votre classe est dans un Namespace

3 - Méthode de recherche (appel entrant / sortant)

Version supérieure à 2.0

La recherche reçoit un JSON sérialisé en entrée et renvoie une Liste d’Id.
Le json contient actuellement deux valeurs :

				
					

{
    "e164": "[numéro_appelant]" ,  // numéro en String de l'appelant, ex : 33123456789
    "numSVI": "[numéro_appelé]"    // numéro en String du SVI appelé
}


				
			

Le retour doit être une List<Id> même si il n’y a qu’un seul élément.

Exemple de redéfinition

Dans cet exemple, on souhaite créer un Case et l’ouvrir à la place du Contact.
On va exécuter le code parent, créer le Case et renvoyer l’Id de ce case.

				
					global class MyAxialysExtension extends AxiaVM.Extension {

    /**
     * @description Retourne une liste d'Id d'objet à ouvrir.
     *
     * - un seul élément retourné sera ouvert
     * - plusieurs va déclencher une ouverture de la recherche avec le numéro de téléphone
     *
     * @param   params   Chaine en Json sérialisé qui contient les paramètres
     * @return  List<Id>
     */
    global override List<Id> getIdsByPhone(String params) {

        // si je veux récupérer les champs du paramètre js
        Map<String, Object> tmp = (Map<String, Object>) JSON.deserializeUntyped(params);
        String e164 = (String) tmp.get('e164');
        String numSVI = (String) tmp.get('numSVI');

        // On peut faire du travail en amont

        // On peut récupérer le résultat de la méthode parente
        List<Id> s = super.getIdsByPhone(params);

        // On peut aussi faire quelque chose après pour changer le retours ou ajouter un Objet
        Case c = new Case(
            Subject = 'Appel en cours',
            Origin = 'Phone',
            ContactId = s[0],
            Status = 'Working',
            SVI__c = numSVI
        );
        insert c;

        return new List<Id>{ c.Id };
    }

}

				
			

Version supérieure à 2.30

Vue globale de la classe SeekPhone

Dans le connecteur du Voice-management, SeekPhone est utilisé afin de rechercher le numéro de l’appelant dans les différents objets possibles (Account, Contact, Lead) et de retourner le résultat trouvé à la fenêtre du Voice-management.
Selon les options on peut au choix :

  • créer la fiche si on ne la trouve pas
  • ouvrir une fenêtre de recherche
  • ne rien faire du tout.

 

				
					global class SeekPhone {
    /**
     * Recherche la fiche de l'appelant et permet plusieurs comportement différents en retour
     * @param js JSON Sérialisé, (voir plus bas)
     * @return JSON sérialisé. Il est conseillé d'utiliser le helper SeekPhone.response
     */
    public static String get(string js) {
        // travail de recherche
        return SeekPhone.response('OBJECT', (String) objectId);
    }

    /**
     * Helper qui fournit une réponse en JSON sérialisé pour retours de la méthode `get`
     * @params type Type d'ouverture que l'on va demander au voicemanagement pour l'appel
     *         valeurs possibles : 'OBJECT'|'SEARCH'|'URL'|'NOTHING'
     * @param value id ou numéro de téléphone à rechercher
     */
    global static String  response (String type, String value) {
        return json.serialize(new Map<String, Object> {
            'type' => type,
            'values' => new List<String>{value}
        });
    }
}

				
			
  • Le retour conditionne l’affichage en sortie du Voice-management :
  • OBJECT : Ouvre un objet Salesforce peut importe sa classe. values est un tableau d’un seul élément qui contient l’id de l’objet.
  • SEARCH : Ouvre la recherche peut importe le résultat. values est un tableau d’un seul élément qui contient le critère à recherche (par exemple le numéro de l’appelant).
  • URL : Ouvre une page sur un composant Lightning de votre organisation. values est un tableau d’un seul élément qui contient l’url à ouvrir.
  • NOTHING : Ne fait rien du tout.

 

Redéfinition du comportement de SeekPhone.get

Pour faire du travail en amont, en aval, ou à la place de SeekPhone.get, il est possible d’utiliser l’héritage de cette sorte

				
					global class MyAxialysExtension extends AxiaVM.Extension {
    global override String seekPhone(String js) {
        // ici je peux faire du travail en amont
        [...]

        // execution du code parent (optionnel)
        String s = super.seekPhone(js);

        // ici je peux faire du travail en aval
        [...]

        // selon mon retour je peux changer complètement le comportement
        return AxiaVM.SeekPhone.response('SOBJECT|URL|SEARCH...', value);
    }
}

				
			

Les éléments présents dans la variable js sont les suivants :

				
					{
    "e164": "33123456789",
    "numSVI": "33800800800",
    "inOut": "in/out"
}

				
			

Exemple d'ouverture d'un case (version sup 2.30)

				
					global class CaseExtensionExample extends AxiaVM.Extension{

    global override String seekPhone(String params) {
       
        // We need the results of parent call
        String previous = super.seekPhone(params);

        Map<String, Object> res = (Map<String, Object>) JSON.deserializeUntyped(previous);
        String result_type = (String) res.get('type');
        List<Object> result_values = (List<Object>) res.get('values');

       
        if( result_type == 'SOBJECT') {
            Id theId = (ID)result_values[0];
           
            // break if the object is neither an account or a contact
            if (!compare(theId.getSobjectType(), 'Account') && !compare(theId.getSobjectType(), 'Contact') ){
                return previous;
            }
           
            // get lasts open cases
            List<Case> open_cases = [SELECT Id FROM Case WHERE
                (AccountId = :theID OR ContactId = :theID)
                AND isClosed=False AND isDeleted = False
                ORDER BY CreatedDate DESC NULLS LAST
            ];
           
            // if I have one or more result, I open the most recent one
            if (open_cases.size() > 0) {
                return AxiaVM.SeekPhone.response('SOBJECT', (String) open_cases[0].Id);
            }
            // if I want to create Case if no cases opened
            /*else {
                Case new_one = Case(...);
                insert new_one;
                return AxiaVM.SeekPhone.response('SOBJECT', (String) new_one.Id);
            }*/
        }

       
        return previous;
    }

    /**
    * Helper that compare the type of the object
    */
    private boolean compare(Schema.SObjectType compare, String checkType){
        try{
            Schema.SObjectType targetType = Schema.getGlobalDescribe().get(checkType);
            if(targetType == null){
                return false;
            }else if( compare == targetType){
                return true;
            }else{
                return false;
            }
        }catch(Exception e){
            //handle exception
                    return false;
        }
    }

}


				
			

4 - Méthode d’ajout de Tâche

Version supérieure à 2.0

La recherche reçoit un JSON sérialisé en entrée et renvoie une Liste d’Id.
Le json contient actuellement deux valeurs :

				
					

{
    "e164": "[numéro_appelant]" ,       // numéro en String de l'appelant, ex : 33123456789
    "dt": "[date_appel]",               // date de l'appel au format DateTime apex
    "id_appel": "[id_appel_voice]",     // id de l'appel du voice management
    "inOut": "[appel_entrant_sortant]", // "in" ou "out" sens de l'appel
    "op_email": "[email_operateur]",    // email de l'opérateur pour attribution
    "groupe": "[groupe]",               // nom du groupe qui à eu l'appel
    "duree": "[temps_de_l_appel]",      // temps de l'appel en secondes
    "rec": "[presence_enregistrement]"  // Booléen qui détermine la présence d'un enregistrement
}


				
			

Le retour doit être l’Id de la tache.

 

Exemple de redéfinition

Dans cet exemple, on souhaite ajouter la tache à l’appel et non au Contact de l’appelant. On va exécuter le code de la recherche de l’apelant et trouver le Case associé et y attacher la tâche.

				
					

global class MyNewExtension extends AxiaVM.Extension {

    global override Id recordTask(String params) {

        Map<String, Object> tmp = (Map<String, Object>) JSON.deserializeUntyped(params);
        String e164 = (String) tmp.get('e164');
        String op_email = (String) tmp.get('op_email');
        String op_email = (String) tmp.get('op_email');
        Datetime date = (Datetime) tmp.get('dt');
        String inout = (String) tmp.get('inOut');

        User[] operator = [SELECT Id FROM User WHERE FederationIdentifier =: op_email ];
        List<List<Contact>> tmp = [FIND :e164 IN PHONE FIELDS  RETURNING Contact(Id)];
        Id id_contact = tmp[0].Id;
        Case c = [SELECT Id FROM Case WHERE ContactId =: id_contact];

        Task task = new Task(
            OwnerId = operator.Id,
            CreatedDate = date,
            WhatId = c.Id,
            Subject = 'appel ' + inout
            //[...]
        );
        insert task;

        return new List<Id>{ task.Id };
    }
    
}


				
			

Version supérieure à 2.30

Depuis cette version il existe deux JSON différent en fonction d’une tâche d’appel ou d’une tâche d’envoi de sms :

JSON de tâche d’appel

				
					{
    "e164": "numero appelant",
    "id_call": "id de l'appel",
    "inOut": "in|out",
    "group_name": "Nom du groupe",
    "call_duration": 123, // en secondes
    "wait_duration": 0,   // en secondes
    "type": 'CALL',
    "numSVI": "numéro du SVI",
    "objectType": "Classe de l'objet dans lequel l'opérateur est ex : Account|Contact|Lead|Case...",
    "recordId": "Id de l'objectType",
    "url": "URL de l'objectType",
    "mode": "task|cron",
    "op_mail": "operator@email",
    "begin_ts":  159269489, // timestamp du début de l'appel
    "end_ts": 159269489,    // timestamp de fin de l'appel
    "transferred_to": "info de transfert"
}

				
			

JSON de tâche d’un SMS

				
					{
    "e164": "numero appelant",
    "content": "contenu du sms (optionnel)",
    "success": true|false, // si tout c'est bien passé pendant l'envoie
    "objectType": "Classe de l'objet dans lequel l'opérateur est ex : Account|Contact|Lead|Case...",
    "recordId": "Id de l'objectType",
    "url": "URL de l'objectType",
    "type": "SMS",
    "mode": "task",
    "op_mail": "operator@email"
}

				
			

Exemple de redéfinition d’une Tache

Dans cet exemple, on souhaite ajouter la tache à l’appel et non au Contact de l’appelant. On va exécuter le code de la recherche de l’appelant et trouver le Case associé et y attacher la tâche.

				
					global class MyNewExtension extends AxiaVM.Extension {

    global override Id recordTask(String params) {

        Map<String, Object> tmp = (Map<String, Object>) JSON.deserializeUntyped(params);
        String e164 = (String) tmp.get('e164');
        String op_mail = (String) tmp.get('op_mail');
        String inout = (String) tmp.get('inOut');
        String type = (String) tmp.get('type');
        //[...]

        if (type == 'CALL') {
            User[] operator = [SELECT Id FROM User WHERE FederationIdentifier =: op_mail ];
            List<List<Contact>> tmp = [FIND :e164 IN PHONE FIELDS  RETURNING Contact(Id)];
            Id id_contact = tmp[0].Id;
            Case c = [SELECT Id FROM Case WHERE ContactId =: id_contact];

            Task task = new Task(
                OwnerId = operator.Id,
                WhatId = c.Id,
                Subject = 'appel ' + inout == 'in' ? 'entrant' : 'sortant'
                //[...]
            );
            insert task;

            return task.Id ;
        }
        return super.recordTask(params)
    }

}

				
			

Exemple pour gérer le personalAccount

Dans cet exemple on ne souhaite pas rajouter de tâches, et créer un personnal account si on n’a aucune correspondance en base.

				
					global with sharing class PersonnalAccountExtension extends AxiaVM.Extension {
    
    private static Pattern numPattern = Pattern.compile('^(([a-zA-Z]+:)?\\d{1,16})|([a-zA-Z]+)$');
    
    
    global override Id recordTask(String params) {
        // Don’t create task
        return null ;
    }

     
   global override String seekPhone(String params) {
        // We need the results of parent call
        // 
        Map<String, Object> tmp = (Map<String, Object>) JSON.deserializeUntyped(
      		params
    	);
        String e164 = (String) tmp.get('e164');
         
        String previous = super.seekPhone(params);
        Map<String, Object> res = (Map<String, Object>) JSON.deserializeUntyped(previous);
        String result_type = (String) res.get('type');
        List<Object> result_values = (List<Object>) res.get('values');

        if( result_type == 'SOBJECT') {
            return previous;
        } else {

            Id personAccountRecordTypeId =  Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName().get('PersonAccount').getRecordTypeId();
            Account account = new Account();
            account.RecordTypeId = personAccountRecordTypeId;
            account.FirstName = 'Axialys';
            account.LastName = 'Axialys';
            account.PersonMailingStreet='Paris';    // fill with contact details
            account.PersonMailingPostalCode='75015';
            account.PersonMailingCity='Paris';
            account.PersonHomePhone=e164;
            account.PersonMobilePhone=e164;
	    insert account;
            System.debug('CreationOK');
            return AxiaVM.SeekPhone.response('SOBJECT', account.ID);       
        }         
       
  }
}



				
			

5 - Déclarer ma classe d’extension

Procédure temporaire, dans une version ultérieure sera ajouté un sélecteur dans la page de paramétrage du Voice Management.

On va signaler au connecteur VoiceManagement qu’il y a une classe d’extension.

Il faut pour ce faire définir un nouvel objet dans la table AxiaVM__ConfigCTI__c.
Par exemple pour une classe MyNewExtension qui étend AxiaVM.Extension, il faut créer un objet AxiaVM__ConfigCTI__cName = "redefined_class" et AxiaVM__value__c = "MyNewExtension".

6 - Conseils pour analyser un soucis

Nous vous conseillons dans un premier temps de vous assurer que la classe que vous souhaitez utiliser surcharge bien celle par défault. Si vous créez vos propres objets, bien configurer à « Aucun » l’objet à crééer. Ensuite, assurez-vous que vous n’auriez pas mis en place des contrôles qui empêcheraient la création de l’objet que vous souhaiteriez créer.

S’assurer que votre votre classe surcharge bien celle d’origine
  • Assurez-vous que le nom de la classe créé surcharge bien la méthode d’origine en ajoutant un “System.debug”, ou regardez dans les logs de la “developer console”, le chargement, vous devriez voir des lignes du style 12:15:18:373 VARIABLE_SCOPE_BEGIN [22]|this|PersonnalAccountExtension|true|false avec le nom de la classe surchargée.
  • Si vous ne voyez pas votre classe, merci de bien vérifier via le workbench ou via la console en lançant la commande suivante qu’il n’y a bien qu’une classe “redefined_class” et qu’elle est bien renseignée avec le nom que vous avez donné à votre classe.
				
					Select Id, Name,AxiaVM__value__c From AxiaVM__ConfigCTI__c Where Name = 'redefined_class'





				
			
S’assurer que vous n’avez pas mis en place des contrôles bloquants

Contrôles qui seraient susceptibles d’empêcher de faire certaines opérations, qui ne seraient pas définies, si par exemple vous avez rendu obligatoire la configuration d’une adresse sur une fiche client.

Enfin, afin de vous aider à debuger, vous avez la possiblité de visualiser le statut de traitement des échanges avec salesforces en ouvrant l’écran de développement de votre navigateur. Vous pourrez vérifier le statut de retour, et en cas d’échec l’erreur ayant provoqué le soucis.