Ostatnio w projekcie dostałem zadanie aby zaimplementować możliwość zmiany obecnego approvera przez innego użytkownika.
Szybko odnalazłem w dokumentacji interesujące mnie procesy, metody i klasy. Okazało się jednak, że nie będzie to takie proste. Niestety aby wykonać reassign na procesie należy mieć uprawnienia typu Modify All. Jest to ryzykowne, aby dawać użytkownikowi tak duże uprawnienia, jednak z powodu braku innego pomysłu pierwsza implementacja polegała na czasowym daniu Permission Seta z Modify All na rekordzie a przy wyjściu do strony głównej ów Permission Set był zabierany. Niby proste, ale nie było to zbyt bezpieczne. Dodatkowo metody zmieniające stan approvala musiały być wykonane w innej transakcji aby uniknąć MIXED DML exception. Sprawa się komplikowała. Znalazłem jednak temat, który poruszał podobną kwestię. Pomysł okazał się genialny w swojej prostocie. W skrócie polegał na wykorzystaniu Email Service do wykonywania pewnych czynności w kontekście użytkownika z uprawnieniami Admina.
Aby uniknąć powyższych problemów należy zmianę Approvera wykonywać na koncie Administratora lub obecnego Approvera. Rozszerzenie uprawnień użytkownika jest bardzo niebezpieczne, dlatego można w tym celu wykorzystać fakt, że Email Service może działać w kontekście konta Administratora.
W tym celu należy utworzyć nowy Email Service, wygenerować nowy adres email, pod którym ów serwis będzie funkcjonował. Następnie musimy utworzyć klasę, która zaimplementuje interfejs Messaging.InboundEmailHandler a konkretnie metodę handleInboundEmail. W tej metodzie zawieramy właściwą logikę polegającą na podmianie aktualnego approvera na nowego. Jedyne czego potrzebujemy to ID rekordu oraz ID nowego i starego approvera. Jak to zrobić? Wystarczy zserializować te dane w treści emaila. W naszym wypadku w metodzie changeApprovalAction przekazujemy mapę z powyższymi ID i ją serializujemy do ciała emaila. Tak przygotowany email wysyłamy na adres stworzonego Email Service.
Odebranego emaila deserializujemy w metodzie handleInboundEmail po czym odczytujemy jego parametry (czyli przekazane ID). Wywołanie zmiany approvera nie rzuci nam już wyjątkiem, ponieważ cała operacja z metody handleInboundEmail wykonywana jest już w kontekście Administratora.
Konfiguracja sprowadza się do domyślnego ustawienia nowego Email Service i wskazania mu klasy Apexowej, która implementuje interfejs Messaging.InboundEmailHandler
Ostatnim krokiem jest wygenerowanie nowego adresu email oraz ustawienie Context Usera. W to miejsce najlepiej dać użytkownika z uprawnieniami Administratora.
global with sharing class ChangeApproverService implements Messaging.InboundEmailHandler {
global Messaging.InboundEmailResult handleInboundEmail(Messaging.inboundEmail inboundEmail, Messaging.InboundEnvelope inboundEnvelope) {
Messaging.InboundEmailResult inboundEmailResult = new Messaging.InboundEmailResult();
Map<String, Object> parametersMap = new Map<String, Object>();
try {
parametersMap = (Map<String, Object>) JSON.deserializeUntyped(inboundEmail.plainTextBody);
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Error handling email service and deserialization. Exception :' + e.getMessage());
}
try {
String action = (String) parametersMap.get('Action');
String requestId = (String) parametersMap.get('RecordId');
String newApproverId = (String) parametersMap.get('ApproverId');
if (!String.isBlank(requestId) || !String.isBlank(newApproverId) || (!String.isBlank(action) && action.equals('Reassign'))) {
Util_NoShare.forceReassignApprover(requestId, newApproverId);
}
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Error handling approver change. Exception :' + e.getMessage());
}
return inboundEmailResult;
}
global static Boolean changeApprovalAction(Id requestId, Id newApproverId) {
Map<String, Object> parametersMap = new Map<String, Object>{
'Action' => 'Reassign',
'RecordId' => requestId,
'ApproverId' => newApproverId
};
try {
EmailServicesAddress emailServicesAddress = getEmailServicesAddress();
Messaging.SingleEmailMessage message = generateMessage(emailServicesAddress, 'Change approver', parametersMap);
Messaging.SendEmailResult[] results = Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{
message
});
if (results[0].success) {
System.debug(LoggingLevel.DEBUG, 'Send email to change approver.');
return true;
}
System.debug(LoggingLevel.ERROR, 'Got error sending email to change approver.');
return false;
} catch (Exception e) {
System.debug(LoggingLevel.ERROR, 'Error getting email service. Exception : ' + e.getMessage());
}
return false;
}
private static EmailServicesAddress getEmailServicesAddress() {
List<EmailServicesAddress> emailServicesAddresses = [
SELECT SystemModstamp, RunAsUserId, LocalPart, IsActive, FunctionId, EmailDomainName, AuthorizedSenders
FROM EmailServicesAddress
WHERE IsActive = true
AND LocalPart LIKE 'ChangeApprover'
];
return emailServicesAddresses.size() == 1 ? emailServicesAddresses.get(0) : null;
}
private static Messaging.SingleEmailMessage generateMessage(EmailServicesAddress emailServicesAddress, String subject, Map<String, Object> parametersMap) {
Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();
message.toAddresses = new String[]{
emailServicesAddress.LocalPart + '@' + emailServicesAddress.EmailDomainName
};
message.setSubject(subject);
message.plainTextBody = JSON.serialize(parametersMap);
return message;
}
}
public class Util_NoShare {
public static void forceReassignApprover(Id requestId, Id newApproverId) {
List<ProcessInstance> processInstances = [
SELECT Id,TargetObjectid, Status, (SELECT Id,Actor.Name FROM Workitems), (SELECT Id, StepStatus, Comments,Actor.Name FROM Steps)
FROM ProcessInstance
WHERE TargetObjectId = :requestId
];
if (processInstances.size() > 0) {
if (processInstances[0].Workitems.size() > 0) {
ProcessInstanceWorkitem currentItem = processInstances[0].Workitems[0];
Id currentApprover = processInstances[0].Workitems[0].ActorId;
if (currentApprover != newApproverId) {
currentItem.ActorId = newApproverId;
update currentItem;
}
}
}
}
}