예제 #1
0
        /// <summary>
        /// Method that creates the Open Shift Entity Mapping, posts to Graph, and saves the data in Azure
        /// table storage upon successful creation in Graph.
        /// </summary>
        /// <param name="accessToken">The Graph access token.</param>
        /// <param name="openShiftNotFound">The open shift to post to Graph.</param>
        /// <param name="monthPartitionKey">The monthwise partition key.</param>
        /// <param name="mappedTeam">The mapped team.</param>
        /// <returns>A unit of execution.</returns>
        private async Task CreateEntryOpenShiftsEntityMappingAsync(
            string accessToken,
            List <OpenShiftRequestModel> openShiftNotFound,
            string monthPartitionKey,
            TeamToDepartmentJobMappingEntity mappedTeam)
        {
            this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync start at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");

            // This foreach loop iterates over the OpenShifts which are to be added into Shifts UI.
            foreach (var item in openShiftNotFound)
            {
                this.telemetryClient.TrackTrace($"Processing the open shift entity with schedulingGroupId: {item.SchedulingGroupId}");

                // create entries from not found list
                var telemetryProps = new Dictionary <string, string>()
                {
                    { "SchedulingGroupId", item.SchedulingGroupId },
                };

                var requestString = JsonConvert.SerializeObject(item);
                var httpClient    = this.httpClientFactory.CreateClient("ShiftsAPI");
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "teams/" + mappedTeam.TeamId + "/schedule/openShifts")
                {
                    Content = new StringContent(requestString, Encoding.UTF8, "application/json"),
                })
                {
                    var response = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);

                    if (response.IsSuccessStatusCode)
                    {
                        var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

                        var openShiftResponse      = JsonConvert.DeserializeObject <Models.Response.OpenShifts.GraphOpenShift>(responseContent);
                        var openShiftMappingEntity = this.CreateNewOpenShiftMappingEntity(openShiftResponse, item.KronosUniqueId, monthPartitionKey, mappedTeam?.RowKey);

                        telemetryProps.Add("ResultCode", response.StatusCode.ToString());
                        telemetryProps.Add("TeamsOpenShiftId", openShiftResponse.Id);

                        this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, telemetryProps);
                        await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(openShiftMappingEntity).ConfigureAwait(false);
                    }
                    else
                    {
                        var errorProps = new Dictionary <string, string>()
                        {
                            { "ResultCode", response.StatusCode.ToString() },
                            { "ResponseHeader", response.Headers.ToString() },
                            { "SchedulingGroupId", item.SchedulingGroupId },
                            { "MappedTeamId", mappedTeam?.TeamId },
                        };

                        // Have the log to capture the 403.
                        this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, errorProps);
                    }
                }
            }

            this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync end at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");
        }
        /// <summary>
        /// Method that will delete an orphaned open shift entity.
        /// </summary>
        /// <param name="allRequiredConfiguration">The required configuration details.</param>
        /// <param name="lookUpDataFoundList">The found open shifts.</param>
        /// <param name="lookUpData">All of the look up (reference data).</param>
        /// <param name="mappedTeam">The list of mapped teams.</param>
        /// <returns>A unit of execution.</returns>
        private async Task DeleteOrphanDataOpenShiftsEntityMappingAsync(
            SetupDetails allRequiredConfiguration,
            List <AllOpenShiftMappingEntity> lookUpDataFoundList,
            List <AllOpenShiftMappingEntity> lookUpData,
            TeamToDepartmentJobMappingEntity mappedTeam)
        {
            // delete entries from orphan list
            var orphanList = lookUpData.Except(lookUpDataFoundList);

            this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync started at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");

            // This foreach loop iterates over items that are to be deleted from Shifts UI. In other
            // words, these are Open Shifts which have been deleted in Kronos WFC, and those deletions
            // are propagating to Shifts.
            foreach (var item in orphanList)
            {
                this.telemetryClient.TrackTrace($"OpenShiftController - Checking {item.RowKey} to see if there are any Open Shift Requests");
                var isInOpenShiftRequestMappingTable = await this.openShiftRequestMappingEntityProvider.CheckOpenShiftRequestExistance(item.RowKey).ConfigureAwait(false);

                if (!isInOpenShiftRequestMappingTable)
                {
                    this.telemetryClient.TrackTrace($"{item.RowKey} is not in the Open Shift Request mapping table - deletion can be done.");
                    await this.DeleteOpenShiftInTeams(allRequiredConfiguration, item, mappedTeam).ConfigureAwait(false);
                }
                else
                {
                    // Log that the open shift exists in another table and it should not be deleted.
                    this.telemetryClient.TrackTrace($"OpenShiftController - Open Shift ID: {item.RowKey} is being handled by another process.");
                }
            }

            this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync ended at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");
        }
        /// <summary>
        /// This method will remove open shifts from Teams in the event we find more OS in cache
        /// than retrieved from Kronos.
        /// </summary>
        /// <param name="allRequiredConfigurations">The required configuration details.</param>
        /// <param name="mappedOrgJobEntity">The team deatils.</param>
        /// <param name="openShiftsToProcess">All of the matching entities in cache.</param>
        /// <param name="numberOfOpenShiftsToRemove">The number of matching entities we want to remove.</param>
        /// <returns>A unit of execution.</returns>
        private async Task RemoveAdditionalOpenShiftsFromTeamsAsync(
            SetupDetails allRequiredConfigurations,
            TeamToDepartmentJobMappingEntity mappedOrgJobEntity,
            List <AllOpenShiftMappingEntity> openShiftsToProcess,
            int numberOfOpenShiftsToRemove)
        {
            var totalSlotsInCache = 0;

            openShiftsToProcess.ForEach(x => totalSlotsInCache += x.KronosSlots);

            if (!openShiftsToProcess.Any() || totalSlotsInCache < numberOfOpenShiftsToRemove)
            {
                // This code should not ever be used in theory however it protects against an infinite loop.
                this.telemetryClient.TrackTrace($"Error when removing open shifts from Teams. We need to remove {numberOfOpenShiftsToRemove} slots from cache but there is only {totalSlotsInCache} remaining in cache.");
                return;
            }

            do
            {
                // Find the mapping entity with the most slots
                var mappingEntityToDecrement = openShiftsToProcess.OrderByDescending(x => x.KronosSlots).First();

                if (mappingEntityToDecrement.KronosSlots - numberOfOpenShiftsToRemove > 0)
                {
                    // More slots than what we need to remove so update slot count in Teams and update cache.
                    var teamsOpenShiftEntity = await this.GetOpenShiftFromTeams(allRequiredConfigurations, mappedOrgJobEntity, mappingEntityToDecrement).ConfigureAwait(false);

                    if (teamsOpenShiftEntity != null)
                    {
                        teamsOpenShiftEntity.SharedOpenShift.OpenSlotCount -= numberOfOpenShiftsToRemove;
                        var response = await this.UpdateOpenShiftInTeams(allRequiredConfigurations, teamsOpenShiftEntity, mappedOrgJobEntity).ConfigureAwait(false);

                        if (response.IsSuccessStatusCode)
                        {
                            mappingEntityToDecrement.KronosSlots -= numberOfOpenShiftsToRemove;
                            await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(mappingEntityToDecrement).ConfigureAwait(false);

                            numberOfOpenShiftsToRemove = 0;
                        }
                    }
                }
                else
                {
                    // We need to remove more so delete the entity in Teams and delete from cache
                    var response = await this.DeleteOpenShiftInTeams(allRequiredConfigurations, mappingEntityToDecrement, mappedOrgJobEntity).ConfigureAwait(false);

                    if (response.IsSuccessStatusCode)
                    {
                        numberOfOpenShiftsToRemove -= mappingEntityToDecrement.KronosSlots;
                    }
                }

                // Remove the mapping entity so we can process the next entity if necessary.
                openShiftsToProcess.Remove(mappingEntityToDecrement);
            }while (numberOfOpenShiftsToRemove > 0);
        }
예제 #4
0
        /// <summary>
        /// This method will cause the thread to wait allowing us to retrun a success response
        /// for the delete WFI request. It will then share the changes.
        /// </summary>
        /// <param name="shift">The shift we want to share.</param>
        /// <param name="mappedTeam">The team details of the schedule we want to share.</param>
        /// <param name="allRequiredConfigurations">The required configuration.</param>
        /// <returns>A unit of execution.</returns>
        private async Task ShareScheduleAfterShiftDeletion(ShiftsShift shift, TeamToDepartmentJobMappingEntity mappedTeam, IntegrationApi.SetupDetails allRequiredConfigurations)
        {
            // We want to wait so that there is time to respond a success to the WFI request
            // meaning the shift will be deleted in Teams.
            Thread.Sleep(int.Parse(appSettings.AutoShareScheduleWaitTime));

            // We now want to share the schedule between the start and end time of the deleted shift.
            await this.graphUtility.ShareSchedule(
                allRequiredConfigurations.GraphConfigurationDetails,
                mappedTeam.TeamId,
                shift.SharedShift.StartDateTime,
                shift.SharedShift.EndDateTime,
                false).ConfigureAwait(false);
        }
        /// <summary>
        /// Method to save or update Teams to Department mapping.
        /// </summary>
        /// <param name="entity">Mapping entity reference.</param>
        /// <returns>http status code representing the asynchronous operation.</returns>
        public async Task <bool> SaveOrUpdateTeamsToDepartmentMappingAsync(TeamToDepartmentJobMappingEntity entity)
        {
            if (entity is null)
            {
                throw new ArgumentNullException(nameof(entity));
            }

            try
            {
                var result = await this.StoreOrUpdateEntityAsync(entity).ConfigureAwait(false);

                return(result.HttpStatusCode == (int)HttpStatusCode.NoContent);
            }
            catch (Exception)
            {
                return(false);

                throw;
            }
        }
예제 #6
0
        /// <summary>
        /// Creates and stores a shift mapping entity.
        /// </summary>
        /// <param name="shift">A shift from Shifts.</param>
        /// <param name="user">A user mapping entity.</param>
        /// <param name="mappedTeam">A team mapping entity.</param>
        /// <param name="monthPartitionKey">The partition key for the shift.</param>
        /// <returns>A task.</returns>
        private async Task CreateAndStoreShiftMapping(ShiftsShift shift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam, List <string> monthPartitionKey)
        {
            var kronosUniqueId     = this.utility.CreateShiftUniqueId(shift, mappedTeam.KronosTimeZone);
            var shiftMappingEntity = this.CreateNewShiftMappingEntity(shift, kronosUniqueId, user.RowKey, mappedTeam.TeamId);

            await this.shiftMappingEntityProvider.SaveOrUpdateShiftMappingEntityAsync(shiftMappingEntity, shift.Id, monthPartitionKey[0]).ConfigureAwait(false);
        }
예제 #7
0
        /// <summary>
        /// Edits a shift in Kronos and updates the database.
        /// </summary>
        /// <param name="editedShift">The shift to edit.</param>
        /// <param name="user">The user the shift is for.</param>
        /// <param name="mappedTeam">The team the user is in.</param>
        /// <returns>A response for teams.</returns>
        public async Task <ShiftsIntegResponse> EditShiftInKronosAsync(ShiftsShift editedShift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam)
        {
            // The connector does not support drafting entities as it is not possible to draft shifts in Kronos.
            // Likewise there is no share schedule WFI call.
            if (editedShift.DraftShift != null)
            {
                return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "Editing a shift as a draft is not supported for your team in Teams. Please publish changes directly using the 'Share' button."));
            }

            if (editedShift.SharedShift == null)
            {
                return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "An unexpected error occured. Could not edit the shift."));
            }

            // We use the display name to indicate shift transfers. As we cannot support editing shifts
            // with a transfer we block edits on shifts containing the transfer string.
            if (editedShift.SharedShift.DisplayName.Contains(appSettings.TransferredShiftDisplayName))
            {
                return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "You can't edit a shift that includes a shift transfer. Please make your changes in Kronos"));
            }

            // We do not support editing activities in Teamsand cannot support editing shift transfers
            // therefore we only expect activities with the regular segment type. Anything else means the
            // manager has modified or added an activity.
            var invalidActivities = editedShift.SharedShift.Activities.Where(x => x.DisplayName != ApiConstants.RegularSegmentType);

            if (invalidActivities.Any())
            {
                return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "Editing shift activities is not supported for your team in Teams."));
            }

            var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false);

            if ((allRequiredConfigurations?.IsAllSetUpExists).ErrorIfNull(editedShift.Id, "App configuration incorrect.", out var response))
            {
                return(response);
            }

            // We need to get all other shifts the employee works that day.
            var kronosStartDateTime = this.utility.UTCToKronosTimeZone(editedShift.SharedShift.StartDateTime, mappedTeam.KronosTimeZone);
            var kronosEndDateTime   = this.utility.UTCToKronosTimeZone(editedShift.SharedShift.EndDateTime, mappedTeam.KronosTimeZone);

            var monthPartitionKey = Utility.GetMonthPartition(this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime));

            var shiftToReplace = await this.shiftMappingEntityProvider.GetShiftMappingEntityByRowKeyAsync(editedShift.Id).ConfigureAwait(false);

            var shiftToReplaceStartDateTime = this.utility.UTCToKronosTimeZone(shiftToReplace.ShiftStartDate, mappedTeam.KronosTimeZone);
            var shiftToReplaceEndDateTime   = this.utility.UTCToKronosTimeZone(shiftToReplace.ShiftEndDate, mappedTeam.KronosTimeZone);

            var commentTimeStamp = this.utility.UTCToKronosTimeZone(DateTime.UtcNow, mappedTeam.KronosTimeZone).ToString(CultureInfo.InvariantCulture);
            var shiftComments    = XmlHelper.GenerateEditedShiftKronosComments(editedShift.SharedShift.Notes, this.appSettings.ShiftNotesCommentText, commentTimeStamp);

            var editResponse = await this.shiftsActivity.EditShift(
                new Uri(allRequiredConfigurations.WfmEndPoint),
                allRequiredConfigurations.KronosSession,
                this.utility.FormatDateForKronos(kronosStartDateTime),
                this.utility.FormatDateForKronos(kronosEndDateTime),
                kronosEndDateTime.Day > kronosStartDateTime.Day,
                Utility.OrgJobPathKronosConversion(user.PartitionKey),
                user.RowKey,
                kronosStartDateTime.TimeOfDay.ToString(),
                kronosEndDateTime.TimeOfDay.ToString(),
                this.utility.FormatDateForKronos(shiftToReplaceStartDateTime),
                this.utility.FormatDateForKronos(shiftToReplaceEndDateTime),
                shiftToReplaceStartDateTime.TimeOfDay.ToString(),
                shiftToReplaceEndDateTime.TimeOfDay.ToString(),
                shiftComments).ConfigureAwait(false);

            if (editResponse.Status != Success)
            {
                return(ResponseHelper.CreateBadResponse(editedShift.Id, error: "Shift could not be edited in Kronos."));
            }

            await this.DeleteShiftMapping(editedShift).ConfigureAwait(false);

            await this.CreateAndStoreShiftMapping(editedShift, user, mappedTeam, monthPartitionKey).ConfigureAwait(false);

            return(ResponseHelper.CreateSuccessfulResponse(editedShift.Id));
        }
예제 #8
0
        /// <summary>
        /// Adds the shift to Kronos and the database.
        /// </summary>
        /// <param name="shift">The shift to add.</param>
        /// <param name="user">The user the shift is for.</param>
        /// <param name="mappedTeam">The team the user is in.</param>
        /// <returns>A response for teams.</returns>
        public async Task <ShiftsIntegResponse> CreateShiftInKronosAsync(ShiftsShift shift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam)
        {
            // The connector does not support drafting entities as it is not possible to draft shifts in Kronos.
            // Likewise there is no share schedule WFI call.
            if (shift.DraftShift != null)
            {
                return(ResponseHelper.CreateBadResponse(shift.Id, error: "Creating a shift as a draft is not supported for your team in Teams. Please publish changes directly using the 'Share' button."));
            }

            if (shift.SharedShift == null)
            {
                return(ResponseHelper.CreateBadResponse(shift.Id, error: "An unexpected error occured. Could not create the shift."));
            }

            if (shift.SharedShift.Activities.Any())
            {
                return(ResponseHelper.CreateBadResponse(shift.Id, error: "Adding activities to shifts is not supported for your team in Teams. Remove all activities and try sharing again."));
            }

            if (user.ErrorIfNull(shift.Id, "User could not be found.", out var response))
            {
                return(response);
            }

            var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false);

            if ((allRequiredConfigurations?.IsAllSetUpExists).ErrorIfNull(shift.Id, "App configuration incorrect.", out response))
            {
                return(response);
            }

            var kronosStartDateTime = this.utility.UTCToKronosTimeZone(shift.SharedShift.StartDateTime, mappedTeam.KronosTimeZone);
            var kronosEndDateTime   = this.utility.UTCToKronosTimeZone(shift.SharedShift.EndDateTime, mappedTeam.KronosTimeZone);

            var commentTimeStamp = this.utility.UTCToKronosTimeZone(DateTime.UtcNow, mappedTeam.KronosTimeZone).ToString(CultureInfo.InvariantCulture);
            var comments         = XmlHelper.GenerateKronosComments(shift.SharedShift.Notes, this.appSettings.ShiftNotesCommentText, commentTimeStamp);

            var creationResponse = await this.shiftsActivity.CreateShift(
                new Uri(allRequiredConfigurations.WfmEndPoint),
                allRequiredConfigurations.KronosSession,
                this.utility.FormatDateForKronos(kronosStartDateTime),
                this.utility.FormatDateForKronos(kronosEndDateTime),
                kronosEndDateTime.Day > kronosStartDateTime.Day,
                Utility.OrgJobPathKronosConversion(user.PartitionKey),
                user.RowKey,
                kronosStartDateTime.TimeOfDay.ToString(),
                kronosEndDateTime.TimeOfDay.ToString(),
                comments).ConfigureAwait(false);

            if (creationResponse.Status != Success)
            {
                return(ResponseHelper.CreateBadResponse(shift.Id, error: "Shift was not created successfully in Kronos."));
            }

            var monthPartitionKey = Utility.GetMonthPartition(this.utility.FormatDateForKronos(kronosStartDateTime), this.utility.FormatDateForKronos(kronosEndDateTime));

            await this.CreateAndStoreShiftMapping(shift, user, mappedTeam, monthPartitionKey).ConfigureAwait(false);

            return(ResponseHelper.CreateSuccessfulResponse(shift.Id));
        }
예제 #9
0
        /// <summary>
        /// Deletes the shift from Kronos and the database.
        /// </summary>
        /// <param name="shift">The shift to remove.</param>
        /// <param name="user">The user the shift is for.</param>
        /// <param name="mappedTeam">The team the user is in.</param>
        /// <returns>A response for teams.</returns>
        public async Task <ShiftsIntegResponse> DeleteShiftInKronosAsync(ShiftsShift shift, AllUserMappingEntity user, TeamToDepartmentJobMappingEntity mappedTeam)
        {
            if (shift.SharedShift == null)
            {
                return(ResponseHelper.CreateBadResponse(shift.Id, error: "An unexpected error occured. Could not delete the shift."));
            }

            if (user.ErrorIfNull(shift.Id, "User could not be found.", out var response))
            {
                return(response);
            }

            var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false);

            if ((allRequiredConfigurations?.IsAllSetUpExists == false).ErrorIfNull(shift.Id, "App configuration incorrect.", out response))
            {
                return(response);
            }

            // Convert to Kronos local time.
            var kronosStartDateTime = this.utility.UTCToKronosTimeZone(shift.SharedShift.StartDateTime, mappedTeam.KronosTimeZone);
            var kronosEndDateTime   = this.utility.UTCToKronosTimeZone(shift.SharedShift.EndDateTime, mappedTeam.KronosTimeZone);

            var deletionResponse = await this.shiftsActivity.DeleteShift(
                new Uri(allRequiredConfigurations.WfmEndPoint),
                allRequiredConfigurations.KronosSession,
                this.utility.FormatDateForKronos(kronosStartDateTime),
                this.utility.FormatDateForKronos(kronosEndDateTime),
                kronosEndDateTime.Day > kronosStartDateTime.Day,
                Utility.OrgJobPathKronosConversion(user.PartitionKey),
                user.RowKey,
                kronosStartDateTime.TimeOfDay.ToString(),
                kronosEndDateTime.TimeOfDay.ToString()).ConfigureAwait(false);

            if (deletionResponse.Status != Success)
            {
                return(ResponseHelper.CreateBadResponse(shift.Id, error: "Shift was not successfully removed from Kronos."));
            }

            await this.DeleteShiftMapping(shift).ConfigureAwait(false);

#pragma warning disable CS4014 // We do not want to await this call as we need the shift to be deleted in Teams before sharing the schedule.
            Task.Run(() => this.ShareScheduleAfterShiftDeletion(shift, mappedTeam, allRequiredConfigurations));
#pragma warning restore CS4014

            return(ResponseHelper.CreateSuccessfulResponse(shift.Id));
        }
예제 #10
0
        /// <summary>
        /// Method that will delete an orphaned open shift entity.
        /// </summary>
        /// <param name="accessToken">The MS Graph Access token.</param>
        /// <param name="lookUpDataFoundList">The found open shifts.</param>
        /// <param name="lookUpData">All of the look up (reference data).</param>
        /// <param name="mappedTeam">The list of mapped teams.</param>
        /// <returns>A unit of execution.</returns>
        private async Task DeleteOrphanDataOpenShiftsEntityMappingAsync(
            string accessToken,
            List <AllOpenShiftMappingEntity> lookUpDataFoundList,
            List <AllOpenShiftMappingEntity> lookUpData,
            TeamToDepartmentJobMappingEntity mappedTeam)
        {
            // delete entries from orphan list
            var orphanList = lookUpData.Except(lookUpDataFoundList);

            this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync started at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");

            // This foreach loop iterates over items that are to be deleted from Shifts UI. In other
            // words, these are Open Shifts which have been deleted in Kronos WFC, and those deletions
            // are propagating to Shifts.
            foreach (var item in orphanList)
            {
                this.telemetryClient.TrackTrace($"OpenShiftController - Checking {item.TeamsOpenShiftId} to see if there are any Open Shift Requests");
                var isInOpenShiftRequestMappingTable = await this.openShiftRequestMappingEntityProvider.CheckOpenShiftRequestExistance(item.TeamsOpenShiftId).ConfigureAwait(false);

                if (!isInOpenShiftRequestMappingTable)
                {
                    this.telemetryClient.TrackTrace($"{item.TeamsOpenShiftId} is not in the Open Shift Request mapping table - deletion can be done.");
                    var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI");
                    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                    using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Delete, "teams/" + mappedTeam.TeamId + "/schedule/openShifts/" + item.TeamsOpenShiftId))
                    {
                        var response = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false);

                        if (response.IsSuccessStatusCode)
                        {
                            var successfulDeleteProps = new Dictionary <string, string>()
                            {
                                { "ResponseCode", response.StatusCode.ToString() },
                                { "ResponseHeader", response.Headers.ToString() },
                                { "MappedTeamId", mappedTeam?.TeamId },
                                { "OpenShiftIdToDelete", item.TeamsOpenShiftId },
                            };

                            this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, successfulDeleteProps);

                            await this.openShiftMappingEntityProvider.DeleteOrphanDataFromOpenShiftMappingAsync(item).ConfigureAwait(false);
                        }
                        else
                        {
                            var errorDeleteProps = new Dictionary <string, string>()
                            {
                                { "ResponseCode", response.StatusCode.ToString() },
                                { "ResponseHeader", response.Headers.ToString() },
                                { "MappedTeamId", mappedTeam?.TeamId },
                                { "OpenShiftIdToDelete", item.TeamsOpenShiftId },
                            };

                            this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, errorDeleteProps);
                        }
                    }
                }
                else
                {
                    // Log that the open shift exists in another table and it should not be deleted.
                    this.telemetryClient.TrackTrace($"OpenShiftController - Open Shift ID: {item.TeamsOpenShiftId} is being handled by another process.");
                }
            }

            this.telemetryClient.TrackTrace($"DeleteOrphanDataOpenShiftsEntityMappingAsync ended at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");
        }
        /// <summary>
        /// Delete an open shift entity from Teams.
        /// </summary>
        /// <param name="allRequiredConfiguration">The required configuration details.</param>
        /// <param name="openShiftMapping">The open shift entity to delete.</param>
        /// <param name="mappedTeam">The team details.</param>
        /// <returns>A unit of execution.</returns>
        private async Task <HttpResponseMessage> DeleteOpenShiftInTeams(SetupDetails allRequiredConfiguration, AllOpenShiftMappingEntity openShiftMapping, TeamToDepartmentJobMappingEntity mappedTeam)
        {
            var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI");

            httpClient.DefaultRequestHeaders.Add("X-MS-WFMPassthrough", allRequiredConfiguration.WFIId);

            var requestUrl = $"teams/{mappedTeam.TeamId}/schedule/openShifts/{openShiftMapping.RowKey}";

            var response = await this.graphUtility.SendHttpRequest(allRequiredConfiguration.GraphConfigurationDetails, httpClient, HttpMethod.Delete, requestUrl).ConfigureAwait(false);

            if (response.IsSuccessStatusCode)
            {
                var successfulDeleteProps = new Dictionary <string, string>()
                {
                    { "ResponseCode", response.StatusCode.ToString() },
                    { "ResponseHeader", response.Headers.ToString() },
                    { "MappedTeamId", mappedTeam?.TeamId },
                    { "OpenShiftIdToDelete", openShiftMapping.RowKey },
                };

                this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, successfulDeleteProps);

                await this.openShiftMappingEntityProvider.DeleteOrphanDataFromOpenShiftMappingAsync(openShiftMapping).ConfigureAwait(false);
            }
            else
            {
                var errorDeleteProps = new Dictionary <string, string>()
                {
                    { "ResponseCode", response.StatusCode.ToString() },
                    { "ResponseHeader", response.Headers.ToString() },
                    { "MappedTeamId", mappedTeam?.TeamId },
                    { "OpenShiftIdToDelete", openShiftMapping.RowKey },
                };

                this.telemetryClient.TrackTrace(Resource.DeleteOrphanDataOpenShiftsEntityMappingAsync, errorDeleteProps);
            }

            return(response);
        }
        /// <summary>
        /// Update an open shift entity in Teams.
        /// </summary>
        /// <param name="allRequiredConfiguration">The required configuration details.</param>
        /// <param name="openShift">The open shift entity to update.</param>
        /// <param name="mappedTeam">The team details.</param>
        /// <returns>A unit of execution.</returns>
        private async Task <HttpResponseMessage> UpdateOpenShiftInTeams(SetupDetails allRequiredConfiguration, GraphOpenShift openShift, TeamToDepartmentJobMappingEntity mappedTeam)
        {
            var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI");

            httpClient.DefaultRequestHeaders.Add("X-MS-WFMPassthrough", allRequiredConfiguration.WFIId);

            var requestString = JsonConvert.SerializeObject(openShift);
            var requestUrl    = $"teams/{mappedTeam.TeamId}/schedule/openShifts/{openShift.Id}";

            var response = await this.graphUtility.SendHttpRequest(allRequiredConfiguration.GraphConfigurationDetails, httpClient, HttpMethod.Put, requestUrl, requestString).ConfigureAwait(false);

            if (response.IsSuccessStatusCode)
            {
                var successfulUpdateProps = new Dictionary <string, string>()
                {
                    { "ResponseCode", response.StatusCode.ToString() },
                    { "ResponseHeader", response.Headers.ToString() },
                    { "MappedTeamId", mappedTeam?.TeamId },
                    { "OpenShiftIdToDelete", openShift.Id },
                };

                this.telemetryClient.TrackTrace("Open shift updated.", successfulUpdateProps);
            }
            else
            {
                var errorUpdateProps = new Dictionary <string, string>()
                {
                    { "ResponseCode", response.StatusCode.ToString() },
                    { "ResponseHeader", response.Headers.ToString() },
                    { "MappedTeamId", mappedTeam?.TeamId },
                    { "OpenShiftIdToDelete", openShift.Id },
                };

                this.telemetryClient.TrackTrace("Open shift could not be updated.", errorUpdateProps);
            }

            return(response);
        }
        /// <summary>
        /// Method that creates the Open Shift Entity Mapping, posts to Graph, and saves the data in Azure
        /// table storage upon successful creation in Graph.
        /// </summary>
        /// <param name="allRequiredConfiguration">The required configuration details.</param>
        /// <param name="openShiftNotFound">The open shift to post to Graph.</param>
        /// <param name="monthPartitionKey">The monthwise partition key.</param>
        /// <param name="mappedTeam">The mapped team.</param>
        /// <returns>A unit of execution.</returns>
        private async Task CreateEntryOpenShiftsEntityMappingAsync(
            SetupDetails allRequiredConfiguration,
            List <OpenShiftRequestModel> openShiftNotFound,
            List <AllOpenShiftMappingEntity> lookUpEntriesFoundList,
            string monthPartitionKey,
            TeamToDepartmentJobMappingEntity mappedTeam)
        {
            this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync start at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");

            // This foreach loop iterates over the OpenShifts which are to be added into Shifts UI.
            foreach (var item in openShiftNotFound)
            {
                this.telemetryClient.TrackTrace($"Processing the open shift entity with schedulingGroupId: {item.SchedulingGroupId}");

                // create entries from not found list
                var telemetryProps = new Dictionary <string, string>()
                {
                    { "SchedulingGroupId", item.SchedulingGroupId },
                };

                var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI");
                httpClient.DefaultRequestHeaders.Add("X-MS-WFMPassthrough", allRequiredConfiguration.WFIId);

                var requestString = JsonConvert.SerializeObject(item);
                var requestUrl    = $"teams/{mappedTeam.TeamId}/schedule/openShifts";

                var response = await this.graphUtility.SendHttpRequest(allRequiredConfiguration.GraphConfigurationDetails, httpClient, HttpMethod.Post, requestUrl, requestString).ConfigureAwait(false);

                if (response.IsSuccessStatusCode)
                {
                    var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

                    var openShiftResponse      = JsonConvert.DeserializeObject <Models.Response.OpenShifts.GraphOpenShift>(responseContent);
                    var openShiftMappingEntity = this.CreateNewOpenShiftMappingEntity(openShiftResponse, item.KronosUniqueId, monthPartitionKey, mappedTeam?.RowKey);

                    telemetryProps.Add("ResultCode", response.StatusCode.ToString());
                    telemetryProps.Add("TeamsOpenShiftId", openShiftResponse.Id);

                    this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, telemetryProps);
                    await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(openShiftMappingEntity).ConfigureAwait(false);

                    // Add the entity to the found list to prevent later processes from deleting
                    // the newly added entity.
                    lookUpEntriesFoundList.Add(openShiftMappingEntity);
                }
                else
                {
                    var errorProps = new Dictionary <string, string>()
                    {
                        { "ResultCode", response.StatusCode.ToString() },
                        { "ResponseHeader", response.Headers.ToString() },
                        { "SchedulingGroupId", item.SchedulingGroupId },
                        { "MappedTeamId", mappedTeam?.TeamId },
                    };

                    // Have the log to capture the error.
                    this.telemetryClient.TrackTrace(Resource.CreateEntryOpenShiftsEntityMappingAsync, errorProps);
                }
            }

            this.telemetryClient.TrackTrace($"CreateEntryOpenShiftsEntityMappingAsync end at: {DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)}");
        }
        /// <summary>
        /// Retrieve an open shift from Teams by Team open shift Id.
        /// </summary>
        /// <param name="allRequiredConfigurations">The required configuration details.</param>
        /// <param name="mappedOrgJobEntity">The team details.</param>
        /// <param name="mappingEntityToDecrement">The mapping entity conatianing the details of the OS we want to retrieve.</param>
        /// <returns>A Graph open shift object.</returns>
        private async Task <GraphOpenShift> GetOpenShiftFromTeams(SetupDetails allRequiredConfigurations, TeamToDepartmentJobMappingEntity mappedOrgJobEntity, AllOpenShiftMappingEntity mappingEntityToDecrement)
        {
            var httpClient = this.httpClientFactory.CreateClient("ShiftsAPI");
            var requestUrl = $"teams/{mappedOrgJobEntity.TeamId}/schedule/openShifts/{mappingEntityToDecrement.RowKey}";

            var response = await this.graphUtility.SendHttpRequest(allRequiredConfigurations.GraphConfigurationDetails, httpClient, HttpMethod.Get, requestUrl).ConfigureAwait(false);

            if (response.IsSuccessStatusCode)
            {
                var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

                return(JsonConvert.DeserializeObject <GraphOpenShift>(responseContent));
            }
            else
            {
                this.telemetryClient.TrackTrace($"The open shift with id {mappingEntityToDecrement.RowKey} could not be found in Teams. ");
                return(null);
            }
        }
        /// <summary>
        /// Generate a Teams open shift entity.
        /// </summary>
        /// <param name="kronosOpenShift">the Kronos open shift object.</param>
        /// <param name="mappedOrgJobEntity">The team details.</param>
        /// <returns>An open shift request model.</returns>
        private OpenShiftRequestModel GenerateTeamsOpenShiftEntity(OpenShiftBatch.ScheduleShift kronosOpenShift, TeamToDepartmentJobMappingEntity mappedOrgJobEntity)
        {
            var openShiftActivity = new List <Activity>();

            // This foreach loop will build the OpenShift activities.
            foreach (var segment in kronosOpenShift.ShiftSegments.ShiftSegment)
            {
                openShiftActivity.Add(new Activity
                {
                    IsPaid        = true,
                    StartDateTime = this.utility.CalculateStartDateTime(segment, mappedOrgJobEntity.KronosTimeZone),
                    EndDateTime   = this.utility.CalculateEndDateTime(segment, mappedOrgJobEntity.KronosTimeZone),
                    Code          = string.Empty,
                    DisplayName   = segment.SegmentTypeName,
                });
            }

            var openShift = new OpenShiftRequestModel()
            {
                SchedulingGroupId = mappedOrgJobEntity.TeamsScheduleGroupId,
                SharedOpenShift   = new OpenShiftItem
                {
                    DisplayName   = string.Empty,
                    OpenSlotCount = Constants.ShiftsOpenSlotCount,
                    Notes         = this.utility.GetOpenShiftNotes(kronosOpenShift),
                    StartDateTime = openShiftActivity.First().StartDateTime,
                    EndDateTime   = openShiftActivity.Last().EndDateTime,
                    Theme         = this.appSettings.OpenShiftTheme,
                    Activities    = openShiftActivity,
                },
            };

            // Generates the uniqueId for the OpenShift.
            openShift.KronosUniqueId = this.utility.CreateUniqueId(openShift, mappedOrgJobEntity);
            return(openShift);
        }
        /// <summary>
        /// Creates and stores a open shift mapping entity.
        /// </summary>
        /// <param name="openShift">An open shift from Shifts.</param>
        /// <param name="mappedTeam">A team mapping entity.</param>
        /// <param name="monthPartitionKey">The partition key for the shift.</param>
        /// <param name="openShiftOrgJobPath">The org job path of the open shift.</param>
        /// <returns>A task.</returns>
        private async Task CreateAndStoreOpenShiftMapping(Models.IntegrationAPI.OpenShiftIS openShift, TeamToDepartmentJobMappingEntity mappedTeam, string monthPartitionKey, string openShiftOrgJobPath)
        {
            var kronosUniqueId = this.utility.CreateOpenShiftInTeamsUniqueId(openShift, mappedTeam.KronosTimeZone, openShiftOrgJobPath);

            var startDateTime = DateTime.SpecifyKind(openShift.SharedOpenShift.StartDateTime, DateTimeKind.Utc);

            AllOpenShiftMappingEntity openShiftMappingEntity = new AllOpenShiftMappingEntity
            {
                PartitionKey            = monthPartitionKey,
                RowKey                  = openShift.Id,
                KronosOpenShiftUniqueId = kronosUniqueId,
                KronosSlots             = openShift.SharedOpenShift.OpenSlotCount,
                OrgJobPath              = openShiftOrgJobPath,
                OpenShiftStartDate      = startDateTime,
            };

            await this.openShiftMappingEntityProvider.SaveOrUpdateOpenShiftMappingEntityAsync(openShiftMappingEntity).ConfigureAwait(false);
        }
        /// <summary>
        /// Creates an open shift in Kronos.
        /// </summary>
        /// <param name="openShift">The open shift entity to create in Kronos.</param>
        /// <param name="team">The team the open shift belongs to.</param>
        /// <returns>A response to return to teams.</returns>
        public async Task <ShiftsIntegResponse> CreateOpenShiftInKronosAsync(Models.IntegrationAPI.OpenShiftIS openShift, TeamToDepartmentJobMappingEntity team)
        {
            // The connector does not support drafting entities as it is not possible to draft shifts in Kronos.
            // Likewise there is no share schedule WFI call.
            if (openShift.DraftOpenShift != null)
            {
                return(ResponseHelper.CreateBadResponse(openShift.Id, error: "Creating an open shift as a draft is not supported for your team in Teams. Please publish changes directly using the 'Share' button."));
            }

            if (openShift.SharedOpenShift == null)
            {
                return(ResponseHelper.CreateBadResponse(openShift.Id, error: "An unexpected error occured. Could not create open shift."));
            }

            if (openShift.SharedOpenShift.Activities.Any())
            {
                return(ResponseHelper.CreateBadResponse(openShift.Id, error: "Adding activities to open shifts is not supported for your team in Teams. Remove all activities and try sharing again."));
            }

            var allRequiredConfigurations = await this.utility.GetAllConfigurationsAsync().ConfigureAwait(false);

            if ((allRequiredConfigurations?.IsAllSetUpExists).ErrorIfNull(openShift.Id, "App configuration incorrect.", out var response))
            {
                return(response);
            }

            var possibleTeams = await this.teamDepartmentMappingProvider.GetMappedTeamDetailsBySchedulingGroupAsync(team.TeamId, openShift.SchedulingGroupId).ConfigureAwait(false);

            var openShiftOrgJobPath = possibleTeams.FirstOrDefault().RowKey;

            var commentTimeStamp = this.utility.UTCToKronosTimeZone(DateTime.UtcNow, team.KronosTimeZone).ToString(CultureInfo.InvariantCulture);
            var comments         = XmlHelper.GenerateKronosComments(openShift.SharedOpenShift.Notes, this.appSettings.ShiftNotesCommentText, commentTimeStamp);

            var openShiftDetails = new
            {
                KronosStartDateTime = this.utility.UTCToKronosTimeZone(openShift.SharedOpenShift.StartDateTime, team.KronosTimeZone),
                KronosEndDateTime   = this.utility.UTCToKronosTimeZone(openShift.SharedOpenShift.EndDateTime, team.KronosTimeZone),
                DisplayName         = openShift.SharedOpenShift.DisplayName,
            };

            var creationResponse = await this.openShiftActivity.CreateOpenShiftAsync(
                new Uri(allRequiredConfigurations.WfmEndPoint),
                allRequiredConfigurations.KronosSession,
                this.utility.FormatDateForKronos(openShiftDetails.KronosStartDateTime),
                this.utility.FormatDateForKronos(openShiftDetails.KronosEndDateTime),
                openShiftDetails.KronosEndDateTime.Day > openShiftDetails.KronosStartDateTime.Day,
                Utility.OrgJobPathKronosConversion(openShiftOrgJobPath),
                openShiftDetails.DisplayName,
                openShiftDetails.KronosStartDateTime.TimeOfDay.ToString(),
                openShiftDetails.KronosEndDateTime.TimeOfDay.ToString(),
                openShift.SharedOpenShift.OpenSlotCount,
                comments).ConfigureAwait(false);

            if (creationResponse.Status != ApiConstants.Success)
            {
                return(ResponseHelper.CreateBadResponse(openShift.Id, error: "Open shift was not created successfully in Kronos."));
            }

            var monthPartitionKey = Utility.GetMonthPartition(
                this.utility.FormatDateForKronos(openShiftDetails.KronosStartDateTime),
                this.utility.FormatDateForKronos(openShiftDetails.KronosEndDateTime));

            await this.CreateAndStoreOpenShiftMapping(openShift, team, monthPartitionKey.FirstOrDefault(), openShiftOrgJobPath).ConfigureAwait(false);

            return(ResponseHelper.CreateSuccessfulResponse(openShift.Id));
        }
        public async Task <IActionResult> ImportMappingAsync()
        {
            var configurationEntities = await this.configurationProvider.GetConfigurationsAsync().ConfigureAwait(false);

            var configEntity = configurationEntities?.FirstOrDefault();

            if (configEntity != null && !string.IsNullOrEmpty(configEntity.WorkforceIntegrationId))
            {
                // Getting the posted file.
                var file = this.HttpContext.Request.Form.Files[0];

                bool isValidFile = true;
                int  noOfColumns = 0;

                if (file != null)
                {
                    using (XLWorkbook workbook = new XLWorkbook(file.OpenReadStream()))
                    {
                        IXLWorksheet worksheet = workbook.Worksheet(1);

                        // Validation to check if row other than column exists
                        if (worksheet.RowsUsed().Count() == 1)
                        {
                            isValidFile = false;
                            return(this.Json(new { isWorkforceIntegrationPresent = true, response = isValidFile }));
                        }

                        // Getting count of total used cells
                        var usedCellsCount = worksheet.RowsUsed().CellsUsed().Count();

                        foreach (IXLRow row in worksheet.RowsUsed())
                        {
                            if (row.RangeAddress.FirstAddress.RowNumber == 1)
                            {
                                // Getting count of total coumns available in the template
                                noOfColumns = row.CellsUsed().Count();
                                continue;
                            }

                            // Validation to check if any cell has empty value
                            if ((usedCellsCount % noOfColumns) != 0 || noOfColumns != Convert.ToInt16(Resources.NoOfColumnsInTeamsExcel, CultureInfo.InvariantCulture))
                            {
                                isValidFile = false;
                                return(this.Json(new { isWorkforceIntegrationPresent = true, response = isValidFile }));
                            }

                            TeamToDepartmentJobMappingEntity entity = new TeamToDepartmentJobMappingEntity()
                            {
                                PartitionKey           = row.Cell(1).Value.ToString(),
                                RowKey                 = Utility.OrgJobPathDBConversion(row.Cell(2).Value.ToString()),
                                KronosTimeZone         = row.Cell(3).Value.ToString(),
                                TeamId                 = row.Cell(4).Value.ToString(),
                                ShiftsTeamName         = row.Cell(5).Value.ToString(),
                                TeamsScheduleGroupId   = row.Cell(6).Value.ToString(),
                                TeamsScheduleGroupName = row.Cell(7).Value.ToString(),
                            };

                            if (isValidFile)
                            {
                                var tenantId     = this.appSettings.TenantId;
                                var clientId     = this.appSettings.ClientId;
                                var clientSecret = this.appSettings.ClientSecret;
                                var instance     = this.appSettings.Instance;

                                var accessToken = await this.graphUtility.GetAccessTokenAsync(tenantId, instance, clientId, clientSecret, configEntity.AdminAadObjectId).ConfigureAwait(false);

                                var graphClient = CreateGraphClientWithDelegatedAccess(accessToken);

                                var isSuccess = await this.graphUtility.AddWFInScheduleAsync(entity.TeamId, graphClient, configEntity.WorkforceIntegrationId, accessToken).ConfigureAwait(false);

                                if (isSuccess)
                                {
                                    await this.teamDepartmentMappingProvider.SaveOrUpdateTeamsToDepartmentMappingAsync(entity).ConfigureAwait(false);
                                }
                            }
                        }
                    }
                }

                return(this.Json(new { isWorkforceIntegrationPresent = true, response = isValidFile }));
            }
            else
            {
                return(this.Json(new { isWorkforceIntegrationPresent = false, error = Resources.WorkforceIntegrationNotRegister }));
            }
        }
        /// <summary>
        /// This method finds any idnetical open shifts before performing logic specific to OS
        /// that occur more than once enabling open shift slot count support.
        /// </summary>
        /// <param name="allRequiredConfigurations">The required configuration details.</param>
        /// <param name="monthPartitionKey">The month partition key currently being synced.</param>
        /// <param name="openShiftsFoundList">The list of Teams open shift entities we found in cache.</param>
        /// <param name="lookUpEntriesFoundList">The list of mapping entities retrieved from Kronos and found in cache.</param>
        /// <param name="mappedOrgJobEntity">The team deatils.</param>
        /// <param name="lookUpData">All of the cache records retrieved for the query date span.</param>
        /// <returns>A unit of execution.</returns>
        private async Task ProcessIdenticalOpenShifts(
            SetupDetails allRequiredConfigurations,
            string monthPartitionKey,
            List <OpenShiftRequestModel> openShiftsFoundList,
            List <AllOpenShiftMappingEntity> lookUpEntriesFoundList,
            TeamToDepartmentJobMappingEntity mappedOrgJobEntity,
            List <AllOpenShiftMappingEntity> lookUpData)
        {
            var identicalOpenShifts = new List <AllOpenShiftMappingEntity>();

            // Count up each individual open shift stored in our cache.
            var map = new Dictionary <string, int>();

            foreach (var openShiftMapping in lookUpData)
            {
                if (map.ContainsKey(openShiftMapping.KronosOpenShiftUniqueId))
                {
                    map[openShiftMapping.KronosOpenShiftUniqueId] += openShiftMapping.KronosSlots;
                }
                else
                {
                    map.Add(openShiftMapping.KronosOpenShiftUniqueId, openShiftMapping.KronosSlots);
                }
            }

            foreach (var item in map)
            {
                if (item.Value > 1)
                {
                    // Where there are more than one identical entity in cache so we require seperate processing.
                    identicalOpenShifts.AddRange(lookUpData.Where(x => x.KronosOpenShiftUniqueId == item.Key));
                }
            }

            // Get each unique hash from the list of open shifts that occur more than once in cache.
            var identicalOpenShiftHash = identicalOpenShifts.Select(x => x.KronosOpenShiftUniqueId).Distinct();

            foreach (var openShiftHash in identicalOpenShiftHash)
            {
                // Retrieve the open shifts to process from cache as well as what we have retrieved from Kronos using the hash.
                var openShiftsInCacheToProcess = identicalOpenShifts.Where(x => x.KronosOpenShiftUniqueId == openShiftHash).ToList();
                var kronosOpenShiftsToProcess  = openShiftsFoundList.Where(x => x.KronosUniqueId == openShiftHash).ToList();

                // Calculate the difference in number of open shifts between Kronos and cache
                var cacheOpenShiftSlotCount = 0;
                openShiftsInCacheToProcess.ForEach(x => cacheOpenShiftSlotCount += x.KronosSlots);
                var numberOfOpenShiftsToRemove = cacheOpenShiftSlotCount - kronosOpenShiftsToProcess.Count;

                if (numberOfOpenShiftsToRemove > 0)
                {
                    // More open shifts in cache than found in Kronos
                    await this.RemoveAdditionalOpenShiftsFromTeamsAsync(allRequiredConfigurations, mappedOrgJobEntity, openShiftsInCacheToProcess, numberOfOpenShiftsToRemove).ConfigureAwait(false);

                    continue;
                }

                if (numberOfOpenShiftsToRemove < 0)
                {
                    // Less open shifts in cache than found in Kronos
                    var openShiftsToAdd = new List <OpenShiftRequestModel>();

                    for (int i = numberOfOpenShiftsToRemove; i < 0; i++)
                    {
                        openShiftsToAdd.Add(kronosOpenShiftsToProcess.First());
                    }

                    await this.CreateEntryOpenShiftsEntityMappingAsync(allRequiredConfigurations, openShiftsToAdd, lookUpEntriesFoundList, monthPartitionKey, mappedOrgJobEntity).ConfigureAwait(false);

                    continue;
                }

                // The number of open shifts in Kronos and Teams matches meaning no action is needed
                continue;
            }
        }