Skip to content

Instantly share code, notes, and snippets.

@zanstaszek9
Created May 27, 2025 11:10
Show Gist options
  • Select an option

  • Save zanstaszek9/ca0af48add22f6fa45ff4b1f847b36fb to your computer and use it in GitHub Desktop.

Select an option

Save zanstaszek9/ca0af48add22f6fa45ff4b1f847b36fb to your computer and use it in GitHub Desktop.
Apex code with invocable action for finding available Time Slots to book in Salesforce Scheduler
@IsTest
public inherited sharing class AvailableTimeSlotsService {
private final static String USED_API_VERSION { get { return '60.0';}}
private final static Integer MAXIMUM_DAYS_TO_SEARCH { get { return 180;}}
@InvocableMethod(Label = 'Find first free Time Slot for Scheduler' IconName='slds:standard:time_period' Description = 'Returns first found unoccupied Time Slot for given Service Territory and Work Type Group.' Category = 'Appointments')
public static List<String> getTimeSlot(final List<SchedulerSearchParams> schedulerSearchParamsList) {
final List<String> freeTimeSlots = new List<String>();
for (SchedulerSearchParams schedulerSearchParam : schedulerSearchParamsList) {
freeTimeSlots.add(getTimeSlot(schedulerSearchParam));
}
return freeTimeSlots;
}
public static String getTimeSlot(final SchedulerSearchParams schedulerSearch) {
List<SlotWrapper> slotWrappers = new List<SlotWrapper>();
final String foundFreeTimeSlot;
final Integer searchOffset = 30; // Days in month
while (slotWrappers.size() <= 0 && schedulerSearch.getOffsetInDays() <= MAXIMUM_DAYS_TO_SEARCH) {
final LxScheduler.GetAppointmentCandidatesInput input = buildInput(schedulerSearch);
final String response = LxScheduler.SchedulerResources.getAppointmentCandidates(input);
slotWrappers = (List<SlotWrapper>) JSON.deserialize(response, List<SlotWrapper>.class);
schedulerSearch.addDaysToOffset(searchOffset); // Prepare to look for next period, in case of nothing was found
}
if(!slotWrappers.isEmpty()) {
slotWrappers.sort();
foundFreeTimeSlot = slotWrappers[0].startTime.split('T')[0];
}
return foundFreeTimeSlot;
}
private static LxScheduler.GetAppointmentCandidatesInput buildInput(final SchedulerSearchParams schedulerSearch) {
LxScheduler.GetAppointmentCandidatesInput input = new LxScheduler.GetAppointmentCandidatesInputBuilder()
.setWorkTypeGroupId(schedulerSearch.workTypeGroupId)
.setTerritoryIds(new List<String>{schedulerSearch.serviceTerritoryId})
.setApiVersion(Double.valueOf(USED_API_VERSION))
.setStartTime(schedulerSearch.startDateWithOffset)
.setEndTime(schedulerSearch.endDateWithOffset)
.build();
return input;
}
public class SchedulerSearchParams {
@InvocableVariable(Required=true)
public Id serviceTerritoryId;
@InvocableVariable(Required=true)
public Id workTypeGroupId;
@InvocableVariable(Required=true)
public Datetime startDate;
@InvocableVariable(DefaultValue = 'Europe/London')
public String timezone;
private Integer offsetInDays = 0;
public final String startDateWithOffset {get {return startDate.addDays(0 + this.offsetInDays).format('yyyy-MM-dd\'T\'HH:mm:ssZ', this.timezone);}}
public final String endDateWithOffset {get {return startDate.addDays(30 + this.offsetInDays).format('yyyy-MM-dd\'T\'HH:mm:ssZ', this.timezone);}}
public SchedulerSearchParams(){}
public SchedulerSearchParams(final Id serviceTerritoryId, final Id workTypeGroupId, final Datetime startDate, final String timezone) {
this.serviceTerritoryId = serviceTerritoryId;
this.workTypeGroupId = workTypeGroupId;
this.startDate = startDate;
this.timezone = timezone;
this.offsetInDays = 0;
}
public SchedulerSearchParams(final Id serviceTerritoryId, final Id workTypeGroupId) {
this(serviceTerritoryId, workTypeGroupId, System.now(), 'Europe/London');
}
public void addDaysToOffset(final Integer days) {
this.offsetInDays += days;
}
public Integer getOffsetInDays() {
return offsetInDays;
}
}
private class SlotWrapper implements Comparable {
public String territoryId {get; set;}
public List<String> resources {get; set;}
public String endTime {get; set;}
public String startTime {get; set;}
public Integer compareTo(Object compareTo) {
SlotWrapper wrapperToCompare = (SlotWrapper) compareTo;
// Date notation is sorted lexicographically, so we can compare them as strings
String startDate1 = startTime.split('T')[0];
String startDate2 = wrapperToCompare.startTime.split('T')[0];
if (startDate1 > startDate2) return 1;
if (startDate1 < startDate2) return -1;
return 0;
}
}
/*** Unit Tests ***/
@TestSetup
public static void setup(){
setConfigRecords();
}
private static void setConfigRecords() {
OperatingHours oh = new OperatingHours(Name = 'Operating Hours', TimeZone = 'Europe/London');
WorkTypeGroup wtg = new WorkTypeGroup(Name = 'Sales Call', GroupType = 'Default', IsActive = true, Description = 'For Prospects');
ServiceTerritory territory = new ServiceTerritory(IsActive = true, Name = 'Virtual', OperatingHours = new OperatingHours(Name = oh.Name));
insert new List<SObject> {oh, wtg, territory};
}
@IsTest
public static void checkThatDatesCorrectlyAssign() {
Id serviceTerritoryId = [SELECT Id FROM ServiceTerritory LIMIT 1].Id;
Id workTypeGroupId = [SELECT Id FROM WorkTypeGroup LIMIT 1].Id;
Datetime today = System.now();
Datetime nextMonth = today.addMonths(1);
setMockSlots(nextMonth, today); // Setting later day as first to confirm dates are sorted correctly
Test.startTest();
SchedulerSearchParams searchParams = new SchedulerSearchParams(serviceTerritoryId, workTypeGroupId);
String timeslot = AvailableTimeSlotsService.getTimeSlot(searchParams);
Test.stopTest();
Assert.areEqual(String.valueOf(today.date()), timeslot, 'Dates should be the same');
}
private static void setMockSlots(Datetime firstDate, Datetime secondDate) {
String firstDateStartTime = firstDate.format('yyyy-MM-dd\'T\'HH:mm:ss', 'Europe/London') + '.000+0000';
String firstDateEndTime = firstDate.addMinutes(30).format('yyyy-MM-dd\'T\'HH:mm:ss', 'Europe/London') + '.000+0000';
String secondDateStartTime = secondDate.format('yyyy-MM-dd\'T\'HH:mm:ss', 'Europe/London') + '.000+0000';
String secondDateEndTime = secondDate.addMinutes(30).format('yyyy-MM-dd\'T\'HH:mm:ss', 'Europe/London') + '.000+0000';
// Sample response taken from https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_lxscheduler_GetAppointmentCandidatesInput.htm#:~:text=This%20example%20shows%20a%20sample%20response%20of%20a%20list%20of%20available%20candidates%3A
String entryFirstDate = '{"startTime":"'+firstDateStartTime+'","endTime":"'+firstDateEndTime+'","resources":["serviceResource1Id"],"territoryId":"serviceTerritoryId"}';
String entrySecondDate = '{"startTime":"'+secondDateStartTime+'","endTime":"'+secondDateEndTime+'","resources":["serviceResource2Id"],"territoryId":"serviceTerritoryId"}';
String expectedResponse = '['+ entryFirstDate + ',' + entrySecondDate +']';
LxScheduler.SchedulerResources.setAppointmentCandidatesMock(expectedResponse);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment