protected override async Task <List <TimeOffModel> > GetSourceRecordsAsync(TeamActivityModel activityModel, ILogger log)
        {
            List <string> wfmEmployeeIds = await CacheHelper.GetWfmEmployeeIdListAsync(_cacheService, activityModel.TeamId).ConfigureAwait(false);

            // get the current set of time off records from the WFM provider for all the employees
            // for the week
            return(await _wfmDataService.ListWeekTimeOffAsync(activityModel.TeamId, wfmEmployeeIds, activityModel.StartDate, activityModel.TimeZoneInfoId));
        }
 protected override async Task ApplyDeltaAsync(TeamActivityModel activityModel, DeltaModel <EmployeeAvailabilityModel> delta, ILogger log)
 {
     // Teams does not support creating availability items, so we must simply do an update
     // instead of a create
     await UpdateDestinationAsync(nameof(delta.Created), activityModel, delta, delta.Created, _teamsService.UpdateAvailabilityAsync, log).ConfigureAwait(false);
     await UpdateDestinationAsync(nameof(delta.Updated), activityModel, delta, delta.Updated, _teamsService.UpdateAvailabilityAsync, log).ConfigureAwait(false);
     await UpdateDestinationAsync(nameof(delta.Deleted), activityModel, delta, delta.Deleted, _teamsService.DeleteAvailabilityAsync, log).ConfigureAwait(false);
 }
        protected async Task UpdateDestinationAsync(string operation, TeamActivityModel activityModel, DeltaModel <T> delta, IEnumerable <T> records, Func <string, T, bool, Task> destinationMethod, ILogger log)
        {
            var tasks = records
                        .Select(record => UpdateDestinationAsync(operation, activityModel, delta, record, destinationMethod, log))
                        .ToArray();

            await Task.WhenAll(tasks);
        }
Example #4
0
        private async Task UpdateScheduleAsync(TeamActivityModel activityModel, DeltaModel <ShiftModel> delta)
        {
            // apply the final delta to the current version of the savedShifts ensuring that no
            // other process can update it while we do so - N.B. the minimum lease time is 15s, the
            // maximum lease time is 1m
            var savedShifts = await _scheduleCacheService.LoadScheduleWithLeaseAsync(GetSaveScheduleId(activityModel.TeamId), activityModel.StartDate, new TimeSpan(0, 0, _options.StorageLeaseTimeSeconds));

            delta.ApplyChanges(savedShifts.Tracked);
            delta.ApplySkipped(savedShifts.Skipped);
            await _scheduleCacheService.SaveScheduleWithLeaseAsync(GetSaveScheduleId(activityModel.TeamId), activityModel.StartDate, savedShifts);
        }
        protected async Task <ResultModel> RunDeltaActivity(TeamActivityModel activityModel, ILogger log)
        {
            log.LogActivity(activityModel);

            /*
             *  before we do anything at all for the activity we should ensure that the cache is populated for this
             *  team because if it is not then we will either end up with:
             *  1. a large number of errors and/or;
             *  2. a larger number of skipped items or;
             *  3. a computed delta of all deletes where all records in the period are deleted in Teams
             */
            var teamEmployeeIds = await _cacheService.GetKeyAsync <List <string> >(ApplicationConstants.TableNameEmployeeLists, activityModel.TeamId).ConfigureAwait(false);

            if (teamEmployeeIds == null || teamEmployeeIds.Count == 0)
            {
                log.LogActivitySkipped(activityModel, "Employee cache not populated.");
                return(new ResultModel());
            }

            var sourceRecords = await GetSourceRecordsAsync(activityModel, log).ConfigureAwait(false);

            log.LogSourceRecords(sourceRecords.Count, activityModel);
            if (sourceRecords.Count == 0 && _options.AbortSyncOnZeroSourceRecords)
            {
                log.LogActivitySkipped(activityModel, "Zero source records returned.");
                return(new ResultModel());
            }

            var savedRecords = await GetSavedRecordsAsync(activityModel, log).ConfigureAwait(false);

            // compute the delta
            var delta = _deltaService.ComputeDelta(savedRecords.Tracked, sourceRecords);

            log.LogFullDelta(activityModel.TeamId, activityModel.DateValue, delta, activityModel.ActivityType);

            if (delta.HasChanges)
            {
                delta.RemoveSkipped(savedRecords.Skipped);
                delta.ApplyMaximum(_options.MaximumDelta);

                log.LogPartialDelta(activityModel.TeamId, activityModel.DateValue, delta, activityModel.ActivityType);

                await SetTeamsIdsAsync(delta, savedRecords, activityModel, log).ConfigureAwait(false);

                // update teams
                await ApplyDeltaAsync(activityModel, delta, log).ConfigureAwait(false);

                log.LogAppliedDelta(activityModel.TeamId, activityModel.DateValue, delta, activityModel.ActivityType);

                await UpdateSavedRecordsAsync(activityModel, savedRecords, delta).ConfigureAwait(false);
            }

            return(delta.AsResult());
        }
 public static void LogShiftError(this ILogger log, Exception ex, TeamActivityModel activityModel, string operationName, ShiftModel shift)
 {
     if (ex is MicrosoftGraphException mex)
     {
         log.LogError(EventIds.Shift, ex, "{shiftType}: Status={status}, OperationName={operationName}, ErrorCode={errorCode}, ErrorDescription={errorDescription}, ErrorRequestId={errorRequestId}, ErrorDate={errorDate}, WfmShiftId={wfmShiftId}, WfmEmployeeId={wfmEmployeeId}, TeamsShiftId={teamsShiftId}, TeamsEmployeeId={teamsEmployeeId}, TeamsGroupId={teamsGroupId}, WfmBuId={wfmBuId}, TeamId={teamId}, WeekDate={weekDate}", activityModel.ActivityType, Status.Failed, operationName, mex.Error.Code, mex.Error.Message, mex.Error.InnerError?.RequestId, mex.Error.InnerError?.Date, shift.WfmShiftId, shift.WfmEmployeeId, shift.TeamsShiftId ?? "Not Set", shift.TeamsEmployeeId ?? "Not Set", shift.TeamsSchedulingGroupId ?? "Not Set", activityModel.WfmBuId, activityModel.TeamId, activityModel.DateValue);
     }
     else
     {
         log.LogError(EventIds.Shift, ex, "{shiftType}: Status={status}, OperationName={operationName}, WfmShiftId={wmfShiftId}, WfmEmployeeId={wfmEmployeeId}, TeamsShiftId={teamsShiftId}, TeamsEmployeeId={teamsEmployeeId}, TeamsGroupId={teamsGroupId}, WfmBuId={wfmBuId}, TeamId={teamId}, WeekDate={weekDate}", activityModel.ActivityType, Status.Failed, operationName, shift.WfmShiftId, shift.WfmEmployeeId, shift.TeamsShiftId ?? "Not Set", shift.TeamsEmployeeId ?? "Not Set", shift.TeamsSchedulingGroupId ?? "Not Set", activityModel.WfmBuId, activityModel.TeamId, activityModel.DateValue);
     }
 }
        public async Task <ResultModel> Run([ActivityTrigger] TeamModel teamModel, ILogger log)
        {
            var activityModel = new TeamActivityModel
            {
                TeamId         = teamModel.TeamId,
                ActivityType   = "Availability",
                TimeZoneInfoId = teamModel.TimeZoneInfoId
            };

            return(await RunDeltaActivity(activityModel, log).ConfigureAwait(false));
        }
Example #8
0
        public async Task <ResultModel> Run([ActivityTrigger] WeekModel weekModel, ILogger log)
        {
            var activityModel = new TeamActivityModel
            {
                TeamId         = weekModel.TeamId,
                DateValue      = weekModel.StartDate.AsDateString(),
                ActivityType   = "Shifts",
                WfmBuId        = weekModel.WfmBuId,
                StartDate      = weekModel.StartDate,
                TimeZoneInfoId = weekModel.TimeZoneInfoId
            };

            return(await RunDeltaActivity(activityModel, log).ConfigureAwait(false));
        }
 private async Task UpdateDestinationAsync(string operation, TeamActivityModel activityModel, DeltaModel <T> delta, T record, Func <string, T, bool, Task> destinationMethod, ILogger log)
 {
     try
     {
         await destinationMethod(activityModel.TeamId, record, false).ConfigureAwait(false);
     }
     catch (ArgumentException)
     {
         delta.SkippedChange(record);
         LogRecordSkipped(activityModel, operation, record, log);
     }
     catch (Exception ex)
     {
         delta.FailedChange(record);
         LogRecordError(ex, activityModel, operation, record, log);
     }
 }
Example #10
0
        protected override async Task <List <ShiftModel> > GetSourceRecordsAsync(TeamActivityModel activityModel, ILogger log)
        {
            var shifts = new List <ShiftModel>();

            // get a manager user for this business unit as only managers can get the schedule data
            var managerIds = await _cacheService.GetKeyAsync <List <string> >(ApplicationConstants.TableNameEmployeeLists, activityModel.WfmBuId).ConfigureAwait(false);

            if (managerIds?.Count > 0)
            {
                var manager = await _cacheService.GetKeyAsync <EmployeeModel>(ApplicationConstants.TableNameEmployees, managerIds[0]).ConfigureAwait(false);

                // get the current set of open shifts from WFM.
                shifts = await _wfmDataService.ListWeekOpenShiftsAsync(activityModel.TeamId, activityModel.WfmBuId, activityModel.StartDate, activityModel.TimeZoneInfoId, manager).ConfigureAwait(false);
            }

            return(shifts);
        }
 protected override void LogRecordError(Exception ex, TeamActivityModel activityModel, string operation, EmployeeAvailabilityModel record, ILogger log)
 {
     log.LogAvailabilityError(ex, activityModel.TeamId, operation, record);
 }
        protected override async Task <CacheModel <EmployeeAvailabilityModel> > GetSavedRecordsAsync(TeamActivityModel activityModel, ILogger log)
        {
            // we are not storing saved records for availability rather we are getting them from the
            // destination (Teams) get all the availability for all employees in the busines unit
            // from Teams api in batches of maximum users
            var teamEmployeeIds = await _cacheService.GetKeyAsync <List <string> >(ApplicationConstants.TableNameEmployeeLists, activityModel.TeamId);

            var cacheModel = new CacheModel <EmployeeAvailabilityModel>();

            foreach (var batch in teamEmployeeIds.Buffer(_options.MaximumUsers))
            {
                var tasks  = batch.Select(empId => _teamsService.GetEmployeeAvailabilityAsync(empId));
                var result = await Task.WhenAll(tasks).ConfigureAwait(false);

                cacheModel.Tracked.AddRange(result.ToList());

                // wait for the configured interval before processing the next batch
                await Task.Delay(_options.BatchDelayMs).ConfigureAwait(false);
            }

            return(cacheModel);
        }
        protected override async Task <List <EmployeeAvailabilityModel> > GetSourceRecordsAsync(TeamActivityModel activityModel, ILogger log)
        {
            var availability = new List <EmployeeAvailabilityModel>();

            List <string> wfmEmployeeIds = await CacheHelper.GetWfmEmployeeIdListAsync(_cacheService, activityModel.TeamId);

            if (wfmEmployeeIds.Count > 0)
            {
                // get the current set of availability records from WFM for all the employees
                availability = await _wfmDataService.ListEmployeeAvailabilityAsync(activityModel.TeamId, wfmEmployeeIds, activityModel.TimeZoneInfoId);
            }
            else
            {
                log.LogInformation($"Awaiting employee cache population for {activityModel.ActivityType} syncronisation.");
            }

            // update with the teams employeeid because this is used as the key in the delta
            foreach (var availabilityItem in availability)
            {
                var employee = await _cacheService.GetKeyAsync <EmployeeModel>(ApplicationConstants.TableNameEmployees, availabilityItem.WfmEmployeeId);

                if (employee != null)
                {
                    availabilityItem.TeamsEmployeeId = employee.TeamsEmployeeId;
                }
            }

            return(availability);
        }
 protected virtual async Task UpdateSavedRecordsAsync(TeamActivityModel activityModel, CacheModel <T> savedRecords, DeltaModel <T> delta)
 {
     delta.ApplyChanges(savedRecords.Tracked);
     delta.ApplySkipped(savedRecords.Skipped);
     await SaveRecordsAsync(activityModel, savedRecords).ConfigureAwait(false);
 }
Example #15
0
 protected override Task SaveRecordsAsync(TeamActivityModel activityModel, CacheModel <ShiftModel> savedRecords)
 {
     // not implemented as the records are saved via the UpdateSavedRecords override instead
     return(Task.CompletedTask);
 }
 protected override Task SaveRecordsAsync(TeamActivityModel activityModel, CacheModel <EmployeeAvailabilityModel> savedRecords)
 {
     // nothing to do, as we are not saving availability records to intermediate storage
     return(Task.CompletedTask);
 }
 protected override async Task SetTeamsIdsAsync(DeltaModel <EmployeeAvailabilityModel> delta, CacheModel <EmployeeAvailabilityModel> savedRecords, TeamActivityModel activityModel, ILogger log)
 {
     // set teams employee ids
     await SetTeamsEmployeeIdsAsync(delta.All.Where(s => string.IsNullOrEmpty(s.TeamsEmployeeId))).ConfigureAwait(false);
 }
Example #18
0
        protected override async Task SetTeamsIdsAsync(DeltaModel <ShiftModel> delta, CacheModel <ShiftModel> savedRecords, TeamActivityModel activityModel, ILogger log)
        {
            var allRecords = delta.All;

            // set teams employee
            await SetTeamsEmployeeIdsAsync(allRecords.Where(s => string.IsNullOrEmpty(s.TeamsEmployeeId)), activityModel.TeamId).ConfigureAwait(false);

            // set job & department name
            var jobLookup = BuildJobLookup(savedRecords.Tracked);

            await SetJobAndDepartmentNameAsync(allRecords, jobLookup, activityModel, log).ConfigureAwait(false);

            // set teams schedule group (N.B this must be set after teams employee id and jobs)
            await SetTeamsSchedulingGroupIdAsync(allRecords.Where(s => string.IsNullOrEmpty(s.TeamsSchedulingGroupId) && !string.IsNullOrEmpty(s.TeamsEmployeeId)), activityModel, log).ConfigureAwait(false);
            await AddEmployeesToSchedulingGroupsAsync(delta, activityModel, log).ConfigureAwait(false);
        }
 protected abstract Task SaveRecordsAsync(TeamActivityModel activityModel, CacheModel <T> savedRecords);
Example #20
0
        protected async Task SetJobAndDepartmentNameAsync(IEnumerable <ShiftModel> shifts, IDictionary <string, JobModel> jobLookup, TeamActivityModel activityModel, ILogger log)
        {
            var activities = shifts
                             .SelectMany(s => s.Jobs)
                             .Where(a => !string.IsNullOrEmpty(a.WfmJobId));

            foreach (var activity in activities)
            {
                if (!jobLookup.TryGetValue(activity.WfmJobId, out var job))
                {
                    try
                    {
                        job = await _wfmDataService.GetJobAsync(activityModel.TeamId, activityModel.WfmBuId, activity.WfmJobId)
                              ?? throw new KeyNotFoundException();

                        jobLookup[activity.WfmJobId] = job;
                    }
                    catch (Exception)
                    {
                        log.LogJobNotFound(activityModel, activity);
                        continue;
                    }
                }

                activity.Code           = job.Name;
                activity.DepartmentName = job.DepartmentName;
                activity.ThemeCode      = job.ThemeCode;
            }

            foreach (var shift in shifts)
            {
                var firstJob = shift.Jobs
                               .OrderBy(j => j.StartDate)
                               .FirstOrDefault();
                shift.DepartmentName = firstJob?.DepartmentName;
                shift.ThemeCode      = firstJob?.ThemeCode;
            }
        }
Example #21
0
 protected override async Task ApplyDeltaAsync(TeamActivityModel activityModel, DeltaModel <ShiftModel> delta, ILogger log)
 {
     await UpdateDestinationAsync(nameof(delta.Created), activityModel, delta, delta.Created, _teamsService.CreateShiftAsync, log).ConfigureAwait(false);
     await UpdateDestinationAsync(nameof(delta.Updated), activityModel, delta, delta.Updated, _teamsService.UpdateShiftAsync, log).ConfigureAwait(false);
     await UpdateDestinationAsync(nameof(delta.Deleted), activityModel, delta, delta.Deleted, _teamsService.DeleteShiftAsync, log).ConfigureAwait(false);
 }
 protected abstract void LogRecordSkipped(TeamActivityModel activityModel, string operation, T record, ILogger log);
 protected abstract void LogRecordError(Exception ex, TeamActivityModel activityModel, string operation, T record, ILogger log);
 protected abstract Task <List <T> > GetSourceRecordsAsync(TeamActivityModel activityModel, ILogger log);
 protected abstract Task <CacheModel <T> > GetSavedRecordsAsync(TeamActivityModel activityModel, ILogger log);
 protected abstract Task ApplyDeltaAsync(TeamActivityModel activityModel, DeltaModel <T> delta, ILogger log);
Example #27
0
 protected override async Task <List <ShiftModel> > GetSourceRecordsAsync(TeamActivityModel activityModel, ILogger log)
 {
     return(await _wfmDataService.ListWeekShiftsAsync(activityModel.TeamId, activityModel.WfmBuId, activityModel.StartDate, activityModel.TimeZoneInfoId).ConfigureAwait(false));
 }
 protected override void LogRecordSkipped(TeamActivityModel activityModel, string operation, EmployeeAvailabilityModel record, ILogger log)
 {
     log.LogAvailabilitySkipped(activityModel.TeamId, operation, record);
 }
Example #29
0
        private async Task AddEmployeesToSchedulingGroupsAsync(DeltaModel <ShiftModel> delta, TeamActivityModel activityModel, ILogger log)
        {
            var allShifts   = delta.All;
            var groupLookup = BuildScheduleGroupLookup(allShifts);

            foreach (var department in groupLookup.Keys)
            {
                // get all the user id's in this department
                var userIds = GetAllUsersInDepartment(allShifts, department);
                if (userIds.Count > 0)
                {
                    try
                    {
                        // and add them to the matching schedule group if necessary
                        await _teamsService.AddUsersToSchedulingGroupAsync(activityModel.TeamId, groupLookup[department], userIds).ConfigureAwait(false);
                    }
                    catch (Exception e)
                    {
                        delta.Created.Concat(delta.Updated).Where(i => i.DepartmentName == department).ForEach(i => delta.FailedChange(i));
                        log.LogSchedulingGroupError(e, activityModel, department, groupLookup[department]);
                        continue;
                    }
                }
            }
        }
 protected abstract Task SetTeamsIdsAsync(DeltaModel <T> delta, CacheModel <T> savedRecords, TeamActivityModel activityModel, ILogger log);