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); }
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)); }
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); } }
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); }
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); }
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);
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; } }
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);
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); }
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);