Created
May 27, 2025 11:10
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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