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