I have all the permission sets updated correctly as I i can hardcode a string 'Test' where the select statement is, but when i put in the select statement, My chat bot immediately closes. Could someone take a quick look? I am trying to test that I can take an email address, search the contact object and return the appDecision (Custom Field)
public with Sharing class GetAdmissionStatus {
public class DecisionOutput {
#InvocableVariable( required=true )
public String aDecision;
}
public class DecisionInput {
#InvocableVariable( required=true )
public String applyEmail;
}
#InvocableMethod(label='Get Admission Status')
public static List < DecisionOutput > GetAdmissionStatus( List < DecisionInput > listDecisionInputs ) {
List < DecisionOutput > objOutputs = new List < DecisionOutput >();
DecisionOutput objOutput = new DecisionOutput();
Set < String > strapplyEmails = new Set < String >();
for ( DecisionInput objDecisionInput : listDecisionInputs )
strapplyEmails.add( objDecisionInput.applyEmail );
objOutput.aDecision = [ SELECT AppDecision__c FROM Contact WHERE Email IN: strapplyEmails LIMIT 1].AppDecision__c;
objOutputs.add( objOutput );
return objOutputs;
}
}
Question is unclear. You have a query in [brackets] and you want to see a text version of it? Try with
String q = Database.getQueryLocator([SELECT Id FROM Account LIMIT 5]).getQuery();
System.debug(q);
If you want to cast query results to string this might help
String x = JSON.serializePretty([SELECT Id FROM Account LIMIT 5]);
// you can do JSON.serialize() too
System.debug(x);
If the bot closes there's high chance some exception is thrown. Try to play with the bot with DeveloperConsole open, see if it throws anything.
And did you give the "bot user" permission to use this apex class, contact object and the fields? Search in https://developer.salesforce.com/docs/atlas.en-us.bot_cookbook.meta/bot_cookbook/bot_cookbook_call_apex_action.htm for "permission set"
Related
I have an Apex class whose purpose it is to retrieve and delete overdue tasks on the contact role (related to an account) that the user just called. I need to modify it so that it queries for all overdue tasks assigned to the user on ALL contact roles on the account but am struggling to get the right query.
Here is a portion of the code in question, the part I think is most relevant:
/***************************************************
Brief Description: Deletes overdue Tasks or Events.
****************************************************/
public class DSDenali_DeleteOverDueActivities {
private static List<Task> followUpTasksToDelete = new List<Task>();
private static List<Event> followUpEventsToDelete = new List<Event>();
private static Map<Id, Set<String>> ownerOfActivities = new Map<Id, Set<String>>();
#InvocableMethod (label = 'DialSource Denali Delete Overdue Activities')
public static void gatherData(List<Data> requests)
{
Map<Id, String> results = new Map<Id, String>();
for (Data request : requests)
results.put(request.contactRoleID, request.assignedTo);
for (Id key : results.keySet())
{
Set<String> assignedToValues = parseAssignedTo(results.get(key));
System.debug('assignedToValues: ' + assignedToValues);
ownerOfActivities.put(key, assignedToValues);
System.debug(ownerOfActivities);
}
queryAndFilterData();
deleteOverdueActivities();
}
//Query for the Tasks and Events and filter the ones to delete
private static void queryAndFilterData()
{
List<Contact_Role__c> contactRoles = [SELECT Id,
(SELECT Id, Owner.UserRole.Name, OwnerId FROM Tasks WHERE status != 'Completed' AND ActivityDate <= :system.TODAY() AND Type__c = 'Outbound Call'),
(SELECT Id, Owner.UserRole.Name, OwnerId, Description FROM Events WHERE EndDateTime <= :system.NOW())
FROM Contact_Role__c
WHERE Id IN :ownerOfActivities.keySet()];
for (Contact_Role__c contactRole : contactRoles)
{
for (Task currentTask : contactRole.Tasks)
{
if (ownerOfActivities.get(contactRole.Id).contains(currentTask.OwnerId))
{
if (currentTask.OwnerId != '0050B000006ET37' && currentTask.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentTask.Owner.UserRole.Name))
followUpTasksToDelete.add(currentTask);
else if (currentTask.OwnerId == '0050B000006ET37')
followUpTasksToDelete.add(currentTask);
else
continue;
}
else if (ownerOfActivities.get(contactRole.Id).contains('ALL'))
{
if (currentTask.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentTask.Owner.UserRole.Name))
followUpTasksToDelete.add(currentTask);
else
continue;
}
}
for (Event currentEvent : contactRole.Events)
{
if (ownerOfActivities.get(contactRole.Id).contains(currentEvent.OwnerId) && currentEvent.Description == NULL)
{
if (currentEvent.OwnerId != '0050B000006ET37' && currentEvent.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentEvent.Owner.UserRole.Name))
followUpEventsToDelete.add(currentEvent);
else if (currentEvent.OwnerId == '0050B000006ET37')
followUpEventsToDelete.add(currentEvent);
else
continue;
}
else if (ownerOfActivities.get(contactRole.Id).contains('ALL') && currentEvent.Description == NULL)
{
if (currentEvent.Owner.UserRole != NULL && Pattern.matches('.*Altair.*', currentEvent.Owner.UserRole.Name))
followUpEventsToDelete.add(currentEvent);
else
continue;
}
}
}
}
//Delete overdue Events/Tasks
private static void deleteOverdueActivities()
{
try{
delete followUpTasksToDelete;
}
catch (DmlException e){
System.debug('The following error occured (DSDenali_DeleteOverDueActivities): ' + e);
}
try{
delete followUpEventsToDelete;
}
catch (DmlException e){
System.debug('The following error occured (DSDenali_DeleteOverDueActivities): ' + e);
}
}
//Parse the CSVs of possible owners
private static Set<String> parseAssignedTo(String assignedTo)
{
Set<String> assignedToValues = new Set<String>();
assignedToValues.addAll(assignedTo.deleteWhitespace().split(','));
return assignedToValues;
}
public class Data
{
#InvocableVariable (required=true)
public String assignedTo;
#InvocableVariable (required=false)
public Id contactRoleID;
}
}
(updated after OP posted more code and asked for code review)
It's not bad code, could use some comments. Consider posting it in https://codereview.stackexchange.com/ (although not many SF-related posts end up there) or on https://salesforce.stackexchange.com
gatherData()
Your input variable (after some parsing) is Map<Id, Set<String>> where key is contact role's id. That set of strings for users (owners) is bit misleading. At a glance you immediately ask yourself why it can't be Set<Id>. Only deep down in the code you see that apparently "All" is one of allowed values. This is... not great. I'd be tempted to make 2 methods here, one taking legit Map<Id, Set<Id>> and other taking simply Set<Id> if you know that you're effectively skipping the second parameter.
queryAndFilterData()
You have only one query and it's not in a loop, very good. My trick (from before edit) won't work for you (you don't really have the Account__c or however is the field called anywere in the input, you have only record ids. If you want to check / delete all tasks for roles under this account cleanest might be to use two queries
// 1st the helper to collect all accounts...
Set<Id> allAccounts = new Map<Id, Account>([SELECT Id
FROM Account
WHERE Id IN (SELECT Account__c FROM Contact_Role__c WHERE Id IN :ownerOfActivities.keyset()]).keyset();
// then the outline of the main query would be bit like this
SELECT Id,
(SELECT ... FROM Tasks WHERE ...),
(SELECT ... FROM Events WHERE ...)
FROM Contact_Role__c
WHERE Account__c IN :allAccounts
AND ...
I'd check how much of this filtering logic could be pushed to the query itself rather than manually inspecting each returned row. I mean look at that:
Imagine we're going the simple route (ignoring concept of "All" users) and let's say you have another Set<Id> allUsers; variable (composed out of all ids mentioned in all your data pieces)
The query for Tasks could become something as simple as
(SELECT Id
FROM Tasks
WHERE Status != 'Completed'
AND ActivityDate <= TODAY
AND Type__c = 'Outbound Call'
AND OwnerId IN :allUsers
AND (OwnerId = '0050B000006ET37' OR Owner.UserRole.Name LIKE '%.Altair.%')
)
You still have to loop through them to verify whether each can really really be deleted (it's not enough to match out of all users, it has to check also if it's OK for user on this particular Contact_Role__c, right?) but something like that should return less rows and no more regular expression matching... should be bit faster.
I wouldn't have a magic variable for that one special owner's id like that. Ideally there would be something else that describes this special user (role? profile? custom field on User record? permission to "Author Apex" in profile?). At the very least move it to helper Id variable at top of the file so it's not copy-pasted all over the place. And ask your business user what should happen if that guy (default task owner? some integration account?) ever leaves the company because poo poo will hit the propeller hard.
If you'll get comfortable with this version of the query the "ALL" version would become even simpler? No "all users", no "magic id" & job done?
(SELECT Id
FROM Tasks
WHERE Status != 'Completed'
AND ActivityDate <= TODAY
AND Type__c = 'Outbound Call'
Owner.UserRole.Name LIKE '%.Altair.%'
)
Don't trust random guy on internet who doesn't know your business process but yeah, there's some room for improvement. Just test thoroughly :)
deleteOverdueActivities()
This is not great try-catch pattern. You just raise it in debug log but silently swallow the error. Make it fail hard (let the error bubble up to user) or do some proper error handling like inserting something to helper Log__c object maybe or sending an email / chatter post to admin...
parseAssignedTo()
Not bad. I expect it explodes horribly when you pass a null to it. You're kind of protected from it by marking the variable required in last lines but I think this annotation applies only to Flows. Calling it from other Apex code isn't protected enough.
I want to add a button to my opportunity header record that is called Insert Products. This will send the opportunity ID to a visualforce page which will have a select file button and an insert button that will loop through the CSV and insert the records to the related opportunity.
This is for non technical users so using Data loader is not an option.
I got this working using standard apex class however hit a limit when i load over 1,000 records (which would happen regularly).
I need to convert this to a batch process however am not sure how to do this.
Any one able to point me in the right direction? I understand a batch should have a start, execute and finish. However i am not sure where i should split the csv and where to read and load?
I found this link which i could not work out how to translate into my requirements: http://developer.financialforce.com/customizations/importing-large-csv-files-via-batch-apex/
Here is the code i have for the standard apex class which works.
public class importOppLinesController {
public List<OpportunityLineItem> oLiObj {get;set;}
public String recOppId {
get;
// *** setter is NOT being called ***
set {
recOppId = value;
System.debug('value: '+value);
}
}
public Blob csvFileBody{get;set;}
public string csvAsString{get;set;}
public String[] csvFileLines{get;set;}
public List<OpportunityLineItem> oppLine{get;set;}
public importOppLinesController(){
csvFileLines = new String[]{};
oppLine = New List<OpportunityLineItem>();
}
public void importCSVFile(){
PricebookEntry pbeId;
String unitPrice = '';
try{
csvAsString = csvFileBody.toString();
csvFileLines = csvAsString.split('\n');
for(Integer i=1;i<csvFileLines.size();i++){
OpportunityLineItem oLiObj = new OpportunityLineItem() ;
string[] csvRecordData = csvFileLines[i].split(',');
String pbeCode = csvRecordData[0];
pbeId = [SELECT Id FROM PricebookEntry WHERE ProductCode = :pbeCode AND Pricebook2Id = 'xxxx HardCodedValue xxxx'][0];
oLiObj.PricebookEntryId = pbeId.Id;
oLiObj.Quantity = Decimal.valueOf(csvRecordData[1]) ;
unitPrice = String.valueOf(csvRecordData[2]);
oLiObj.UnitPrice = Decimal.valueOf(unitPrice);
oLiObj.OpportunityId = 'recOppId';;
insert (oLiObj);
}
}
catch (Exception e)
{
ApexPages.Message errorMessage = new ApexPages.Message(ApexPages.severity.ERROR, e + ' - ' + unitPrice);
ApexPages.addMessage(errorMessage);
}
}
}
First problem that I can sense is that the insert DML statement is inside FOR-loop. Can you put the new "oLiObj" into a List that is declared before the FOR-loop starts and then try inserting the list after the FOR-loop ?
It should bring some more sanity in your code.
This is my fist time with Apex so Im fumbling around. What I am trying to do is read a date field Contract_Expiration__c and then set pdate_active_status_text__c to null, active, or void.
Any help would be great. Thanks in advance.
public class update_active_status {
public static String saveData(){
for (Account a : [SELECT Id FROM Account WHERE CreatedDate > 2000-01-16T10:00:00-08:00]){
if (a.Contract_Expiration__c == null ){
a.update_active_status_text__c = null;
update a;
} else if (a.Contract_Expiration__c >= Date.today().addDays(60)) {
a.update_active_status_text__c = 'Active';
update a;
} else {
a.update_active_status_text__c = 'Void';
update a;
}
}
}
}
You need to add the fields to modify/check to your query in order for that method to work:
[SELECT Id, Contract_Expiration__c, update_active_status_text__c FROM Account WHERE CreatedDate > 2000-01-16T10:00:00-08:00]
Also that method needs to return a String (but the compiler should have complained about it already).
And finally, the approach of selecting all accounts created from a given date does not seem nice on the long run.
If possible, could anyone please direct me towards the right direction? The second code (Batch Apex) is just not compiling. Currently the error is,
Error: Compile Error: Invalid type: updateContactOnEmailOptOutChangeScheduler
at line 63 column 73
But I think there are other issues to that I can't seem to make it right.
On a contact update, the trigger updates all duplicate contacts with new value of "Email Opt Out" if this field has been updated. Also, the trigger only updates duplicate contacts that have a HasOptedOutOfEmail value different from one being updated. Now my task is to convert this requirement from trigger (that was written and tested by my colleague) to Batch Apex. First is the original trigger. Second is the code I just wrote in the format of batch apex.
Original Trigger Code
trigger updateContactOnEmailOptOutChange on Contact (after update) {
//Initialize lists and maps
List<Contact> duplicateContacts = new List<Contact>();
Map<String, Contact> contactEmailMap = new Map<String, Contact>();
Map<Id, Contact> contactIdMap = new Map<Id, Contact>();
//Build a map with contacts to update. Only select the ones that have a different "Email Opt Out" value from the contact being updated.
for (Integer i = 0; i < Trigger.new.size(); i++) {
if (Trigger.old[i].HasOptedOutOfEmail != Trigger.new[i].HasOptedOutOfEmail) {
contactEmailMap.put(Trigger.old[i].email, Trigger.new[i]);
contactIdMap.put(Trigger.old[i].id, Trigger.new[i]);
}
}
//Only go through this process if "Email Opt Out" (HasOptedOutofEmail) was updated.
If (contactIdMap.size()>0) {
//Query the database and look for all contacts with a duplicate email address (same email as the contact currently being updated).
for (Contact dupContact : [SELECT Id, Name, Email, HasOptedOutOfEmail
FROM Contact
WHERE Email IN : contactEmailMap.KeySet()
AND Id NOT IN : contactIdMap.KeySet()]) {
Contact contact = contactEmailMap.get(dupContact.Email);
If (dupContact.HasOptedOutOfEmail <> contact.HasOptedOutOfEmail) {
dupContact.HasOptedOutOfEmail = contact.HasOptedOutOfEmail;
duplicateContacts.add(dupContact);
}
}
//If any duplicate contacts were found, update all duplicate contacts with the new HasOptedOutOfEmail value.
If (duplicateContacts.size()>0) update duplicateContacts;
}
}
BATCH APEX
global class updateContactOnEmailOptOutChange implements Database.Batchable<sObject>
{
global string query;
global updateContactOnEmailOptOutChange()
{
query = 'SELECT id,Name, Email, HasOptedOutofEmail from Contact where HasOptedOutofEmail=true';
}
global Database.QueryLocator start(Database.BatchableContext BC)
{
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, List <sObject> duplicateContacts)
{
Map<String, Contact> contactEmailMap = new Map<String, Contact>();
Map <Id, Contact> contactIdMap = new Map<Id, Contact>();
// Build a map with contacts to update. Only select the ones that have a different "Email Opt Out" value from the contact being updated.
if(trigger.isUpdate){
for(Integer i=0; i<Trigger.new.size();i++)
{
if(Trigger.old[i].HasOptedOutOfEmail != Trigger.new[i].HasOptedOutOfEmail)
{
contactEmailMap.put(Trigger.old[i].email, Trigger.new[i]);
contactIdMap.put(Trigger.old[i].id, Trigger.new[i]);
}
}
if(contactidMap.size()>0)
{
//Query the database and look for all contacts with a duplicate email address(same email as the contact currently being updated)
for (Contact dupContact: [SELECT Id, Name, Email, HasOptedOutofEmail
FROM Contact
WHERE Email IN: contactEmailMap.KeySet()
AND Id NOT IN: contactIdMap.KeySet()])
{
Contact contact=contactEmailMap.get(dupContact.Email);
If(dupContact.HasOptedOutOfEmail <> contact.HasOptedOutOfEmail)
{
dupContact.HasOptedOutOfEmail = contact.HasOptedOutOfEmail;
duplicateContacts.add(dupContact);
}
}
// if any duplicate contacts were found, update all duplicate contacts with the new HasOptedOutOFEmail value.
If(duplicateContacts.size<>0) update duplicateContacts;
}
}
}
//The batch process has completed successfully. Schedule next batch.
global void finish(Database.BatchableContext BC){
// //Build the system time of now + 300 seconds to schedule the batch apex.
Datetime sysTime = System.now();
sysTime = sysTime.addSeconds(300);
String chron_exp=''+sysTime.second()+''+sysTime.minute()+''+sysTime.hour()+''+sysTime.day()+''+sysTime.month()+'?'+sysTime.year();
system.debug(chron_exp);
updateContactOnEmailOptOutChangeScheduler scheduleFieldUpdate = new updateContactOnEmailOptOutChangeScheduler();
//Schedule the next job, and give it the system time so name is unique
System.schedule('New Email Update Job'+sysTime.getTime(),chron_exp,scheduleFieldUpdate);
}
}
Your batch apex is including references to the Trigger class that is only valid inside a trigger. The compile error message says there is an invalid type on line 63. Check the line numbers but that likely points to the reference to Trigger
I am writing a test class for a trigger. But i am not able to run it properly it only contain 68%.
The error is
System.DmlException: Insert failed. First exception on row 0; first error: FIELD_CUSTOM_VALIDATION_EXCEPTION, This part is not forecastable.
#isTest
private class TestTriggers
{
static testMethod void testService()
{
//Insert part
list<Opportunity> Opportunity = new list<Opportunity>();
Opportunity = [Select id from Opportunity];
list<Product2> Product = new list<Product2>();
Product = [Select id from Product2];
Part__c p = new Part__c(Stage__c = 'idea',Product__c=Product[0].id,Opportunity__c=Opportunity[0].id);
insert p;
//Update part
list<part__c> partlist = new list<part__c>();
partlist = [Select Stage__c from part__c where Stage__c = 'idea'];
partlist[0].Stage__c = 'update';
update partlist;/* */
}
}
thanks
Anuraj
There is a validation rule on the Part object. To view the validation rule(s), go to Setup > Create > Objects > Part > Validation Rules.
You'll need to modify your code to create a Part__c record that complies with the validation rules.