Ejemplo n.º 1
0
        /// <summary>
        /// Create a Bookings appointment for the given consult request.
        /// </summary>
        /// <param name="ssoToken">The SSO token of the calling agent.</param>
        /// <param name="request">The consult request to book.</param>
        /// <param name="assignedStaffMemberId">The Bookings staff member ID of the assigned agent.</param>
        /// <param name="azureADSettings">Azure AD configuration settings.</param>
        /// <returns>A <see cref="Task"/> representing the asynchronous operation. The task result contains the created <see cref="GraphBeta.BookingAppointment"/>.</returns>
        public static async Task<GraphBeta.BookingAppointment> CreateBookingsAppointment(string ssoToken, Request request, string assignedStaffMemberId, AzureADSettings azureADSettings)
        {
            var authProvider = CreateOnBehalfOfProvider(azureADSettings, new[] { "BookingsAppointment.ReadWrite.All" });
            var graphServiceClient = new GraphBeta.GraphServiceClient(authProvider);

            var bookingAppointment = new GraphBeta.BookingAppointment
            {
                CustomerEmailAddress = request.CustomerEmail,
                CustomerName = request.CustomerName,
                CustomerPhone = request.CustomerPhone,
                Start = GraphBeta.DateTimeTimeZone.FromDateTimeOffset(request.AssignedTimeBlock.StartDateTime.ToUniversalTime(), System.TimeZoneInfo.Utc),
                End = GraphBeta.DateTimeTimeZone.FromDateTimeOffset(request.AssignedTimeBlock.EndDateTime.ToUniversalTime(), System.TimeZoneInfo.Utc),
                OptOutOfCustomerEmail = false,
                ServiceId = request.BookingsServiceId,
                StaffMemberIds = new List<string> { assignedStaffMemberId },
                IsLocationOnline = true,
            };

            var bookingResult = await graphServiceClient.BookingBusinesses[request.BookingsBusinessId].Appointments
                .Request()
                .WithUserAssertion(new UserAssertion(ssoToken))
                .AddAsync(bookingAppointment);

            return bookingResult;
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Update a Bookings appointment.
        /// </summary>
        /// <param name="ssoToken">The SSO token.</param>
        /// <param name="request">The consult request to book.</param>
        /// <param name="assignedStaffMemberId">The Bookings staff member ID of the assigned agent.</param>
        /// <param name="azureADSettings">Azure AD configuration settings.</param>
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
        public static async Task UpdateBookingsAppointment(string ssoToken, Request request, string assignedStaffMemberId, AzureADSettings azureADSettings)
        {
            var authProvider = CreateOnBehalfOfProvider(azureADSettings, new[] { "BookingsAppointment.ReadWrite.All" });
            var graphServiceClient = new GraphBeta.GraphServiceClient(authProvider);

            var bookingAppointment = new GraphBeta.BookingAppointment
            {
                StaffMemberIds = new List<string> { assignedStaffMemberId },
            };

            await graphServiceClient.BookingBusinesses[request.BookingsBusinessId].Appointments[request.BookingsAppointmentId]
                .Request()
                .WithUserAssertion(new UserAssertion(ssoToken))
                .UpdateAsync(bookingAppointment);
        }
        public async Task <IActionResult> AssignRequestAsync(string consultId, [FromBody] AssignConsultRequestBody requestBody, [FromHeader] string authorization)
        {
            string ssoToken = authorization.Substring("Bearer".Length + 1);

            // Extract user's object ID from claims
            var assignerObjectId = this.GetUserObjectId();
            var assignerUserName = this.GetUserName();

            if (string.IsNullOrWhiteSpace(assignerObjectId))
            {
                this.logger.LogError("Failed to assign consult request. The assigner's object ID was null or empty.");
                return(this.Unauthorized());
            }

            if (string.IsNullOrEmpty(consultId))
            {
                return(this.BadRequest(new UnsuccessfulResponse {
                    Reason = "Invalid consult ID provided"
                }));
            }

            // Handle assign to another agent
            var assigneeObjectId = assignerObjectId;

            if (requestBody.Agent != null && !string.IsNullOrEmpty(requestBody.Agent.Id) && !string.IsNullOrEmpty(requestBody.Agent.DisplayName))
            {
                var canAssignOther = await this.CheckSupervisorAsync(consultId);

                if (!canAssignOther)
                {
                    return(this.Unauthorized());
                }

                assigneeObjectId = requestBody.Agent.Id;
            }

            // Lookup the agent
            var assigneeAgent = await this.agentRepository.GetByObjectIdAsync(assigneeObjectId);

            if (assigneeAgent == null)
            {
                this.logger.LogError("Failed to assign consult request. Agent with ID {UserObjectId} does not exist", assigneeObjectId);
                return(this.BadRequest(new UnsuccessfulResponse {
                    Reason = "Invalid agent provided"
                }));
            }

            // Set assignee user name based on self-assign vs assign to other
            var assigneeUserName = assigneeObjectId == assignerObjectId ? assignerUserName : assigneeAgent.Name;

            // Load existing consult from DB
            var request = await this.requestRepository.GetAsync(new CosmosItemKey(consultId, consultId));

            if (request == null)
            {
                this.logger.LogError("Failed to assign consult request {ConsultRequestId}. The consult request does not exist.", consultId);
                return(this.NotFound(new UnsuccessfulResponse {
                    Reason = "Consult request not found"
                }));
            }

            if (request.Status != RequestStatus.Unassigned && request.Status != RequestStatus.ReassignRequested)
            {
                this.logger.LogWarning("Failed to assign consult request {ConsultRequestId}. The consult request is already assigned.", consultId);
                return(this.StatusCode(403, new UnsuccessfulResponse {
                    Reason = "The consult is already assigned"
                }));
            }

            // Update consult request fields from request body
            request.AssignedTimeBlock = requestBody.SelectedTimeBlock;

            string assigneeStaffMemberId = assigneeAgent.BookingsStaffMemberId;

            GraphBeta.BookingAppointment bookingsResult = null;
            bool didBookingsSucceed          = false;
            bool didRetrieveNewStaffMemberId = false;

            while (!didBookingsSucceed && !didRetrieveNewStaffMemberId)
            {
                // If we don't have the agent's staff member ID, try getting it from Graph
                if (string.IsNullOrEmpty(assigneeStaffMemberId))
                {
                    try
                    {
                        assigneeStaffMemberId = await GraphUtil.GetBookingsStaffMemberId(ssoToken, request.BookingsBusinessId, assigneeAgent.UserPrincipalName, this.azureADOptions.Value);
                    }
                    catch (Graph.ServiceException)
                    {
                        // This failure could also be because the caller doesn't have permission
                        this.logger.LogError("Failed to assign consult request {ConsultRequestId}. Bookings staff member ID for agent {AgentUPN} could not be retrieved.", consultId, assigneeAgent.UserPrincipalName);
                        return(this.BadRequest(new UnsuccessfulResponse {
                            Reason = "The assignee is not a valid staff member in Bookings."
                        }));
                    }

                    if (string.IsNullOrEmpty(assigneeStaffMemberId))
                    {
                        this.logger.LogError("Failed to assign consult request {ConsultRequestId}. Bookings staff member ID for agent {AgentUPN} could not be retrieved.", consultId, assigneeAgent.UserPrincipalName);
                        return(this.BadRequest(new UnsuccessfulResponse {
                            Reason = "The assignee is not a valid staff member in Bookings."
                        }));
                    }

                    didRetrieveNewStaffMemberId = true;
                }

                // Create/update Bookings appointment
                try
                {
                    if (request.Status == RequestStatus.Unassigned)
                    {
                        bookingsResult = await GraphUtil.CreateBookingsAppointment(ssoToken, request, assigneeStaffMemberId, this.azureADOptions.Value);
                    }
                    else
                    {
                        await GraphUtil.UpdateBookingsAppointment(ssoToken, request, assigneeStaffMemberId, this.azureADOptions.Value);
                    }

                    didBookingsSucceed = true;
                }
                catch (Graph.ServiceException)
                {
                    // Will try retrieving a fresh staff member ID
                    assigneeStaffMemberId = null;
                }
            }

            // Ensure appointment creation succeeded
            if (!didBookingsSucceed)
            {
                this.logger.LogError("Failed to assign consult request {ConsultRequestId}. The Bookings appointment could not be created/updated for agent {AgentId}.", consultId, assigneeObjectId);
                return(this.BadRequest(new UnsuccessfulResponse {
                    Reason = "Unable to create/update the Bookings appointment."
                }));
            }

            // Update DB with agent's staff member ID, if needed
            if (assigneeAgent.BookingsStaffMemberId != assigneeStaffMemberId)
            {
                assigneeAgent.BookingsStaffMemberId = assigneeStaffMemberId;
                await this.agentRepository.UpsertAsync(assigneeAgent);
            }

            // Update consult request fields with Bookings info, if needed
            if (request.Status == RequestStatus.Unassigned)
            {
                request.BookingsAppointmentId = bookingsResult.Id;
                request.JoinUri = bookingsResult.OnlineMeetingUrl;
            }

            // Update other consult request fields
            var currentTime = DateTime.UtcNow;

            request.Status         = RequestStatus.Assigned;
            request.AssignedToId   = assigneeObjectId;
            request.AssignedToName = assigneeUserName;
            request.Activities     = request.Activities ?? new List <Activity>();
            request.Activities.Add(new Activity
            {
                Id   = Guid.NewGuid(),
                Type = ActivityType.Assigned,
                ActivityForUserId   = assigneeObjectId,
                ActivityForUserName = assigneeUserName,
                Comment             = !string.IsNullOrWhiteSpace(requestBody.Comments) ? requestBody.Comments : null,
                CreatedByName       = assignerUserName,
                CreatedById         = new Guid(assignerObjectId),
                CreatedDateTime     = currentTime,
            });

            // Update consult request in DB
            await this.requestRepository.UpsertAsync(request);

            // Look up the channel to notify
            var mapping = await this.channelMappingRepository.GetByCategoryAsync(request.Category);

            if (mapping == null)
            {
                this.logger.LogError("Failed to send channel message for assignment of consult {ConsultRequestId}. Channel mapping does not exist for request category {RequestCategory}.", consultId, request.Category);
                return(this.BadRequest(new UnsuccessfulResponse {
                    Reason = "Consult requests in this category cannot be assigned"
                }));
            }

            // Lookup the Channel
            var channel = await this.channelRepository.GetByChannelIdAsync(mapping.ChannelId);

            if (channel == null)
            {
                this.logger.LogError("Failed to send channel message for assignment of consult {ConsultRequestId}. Channel {ChannelId} does not exist, but a mapping to that channel exists.", consultId, mapping.ChannelId);
                return(this.BadRequest(new UnsuccessfulResponse {
                    Reason = "Consult requests in this category cannot be reassigned"
                }));
            }

            // Get the assigned consult adaptive card for channel
            var baseUrl            = $"https://{this.azureADOptions.Value.HostDomain}";
            var consultChannelCard = CardFactory.CreateAssignedConsultAttachment(request, baseUrl, assigneeUserName, false, this.localizer);

            // Update the proactive message to the channel
            await ProactiveUtil.UpdateChannelProactiveMessageAsync(consultChannelCard, channel.ServiceUrl, request.ConversationId, request.ActivityId, this.botOptions.Value);

            // Temporarily set agent's locale as current culture before using localizer
            Microsoft.Bot.Schema.Attachment consultPersonalCard;
            using (new CultureSwitcher(assigneeAgent.Locale, assigneeAgent.Locale))
            {
                // Get the assigned consult adaptive card for 1:1 chat
                consultPersonalCard = CardFactory.CreateAssignedConsultAttachment(request, baseUrl, assigneeUserName, true, this.localizer);
            }

            // Send the proactive message to the agent
            await ProactiveUtil.SendChatProactiveMessageAsync(consultPersonalCard, assigneeAgent.TeamsId, this.azureADOptions.Value.TenantId, assigneeAgent.ServiceUrl, this.botOptions.Value);

            return(this.Ok(request));
        }