Files
houston-be/service/team_service.go
Vijay Joshi ad96361d68 TP-49979 , TP-52174 : API to get resolution tags + resolve incident API + incident resolve entire flow refactor (#347)
* TP-49979 : Added API to get tags for resolving incident

* TP-49979 : Set up basic structure for resolve incident from UI

* TP-49979 : Complete till post rca flow

* TP-49979 : Complete till rca gen flow

* TP-52174 : rebase changes

* TP-52174 : Integrate with slack

* TP-52174 : fix error in flows

* TP-52174 : Segregate interface and impl

* TP-52174 : Fix ut failures

* TP-52174 : Fix resolve tag api error

* TP-52174 : Fix jira link bug

* TP-52174 : Remove nil

* TP-52174 : Rebase changes

* TP-52174 : Jira links table fix

* TP-52174 : Line length fix

* TP-52174 : Makefile changes

* TP-52174 : Basic bug fixes

* TP-52174 : Minor fixes

* TP-52174 : Add UT's for initial flows

* TP-52174 : Added all UT's

* TP-52174 : More PR review changes

* TP-52174 : Add UT's for incident jira and tag service

* TP-52174 : Fix jira link bug and batched create incident tags db call

* TP-52174 : Make auto archival severities configurable

* TP-52174 : Fix jira link in incident table issue
2024-02-01 15:23:15 +05:30

517 lines
20 KiB
Go

package service
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgconn"
"github.com/slack-go/slack"
"github.com/spf13/viper"
"go.uber.org/zap"
"gorm.io/gorm"
commonutil "houston/common/util"
houstonSlackUtil "houston/common/util/slack"
"houston/logger"
"houston/model/log"
"houston/model/team"
"houston/pkg/slackbot"
request "houston/service/request"
service "houston/service/response"
common "houston/service/response/common"
utils "houston/service/utils"
"net/http"
"strconv"
"strings"
)
type TeamService struct {
gin *gin.Engine
db *gorm.DB
client *slackbot.Client
authService *AuthService
}
func NewTeamService(gin *gin.Engine, db *gorm.DB, client *slackbot.Client, authService *AuthService) *TeamService {
return &TeamService{
gin: gin,
db: db,
client: client,
authService: authService,
}
}
func (t *TeamService) AddTeam(c *gin.Context) {
logRepository := log.NewLogRepository(t.db)
teamRepository := team.NewTeamRepository(t.db, logRepository)
minLength := viper.GetInt("TEAM_NAME_MIN_LENGTH")
maxLength := viper.GetInt("TEAM_NAME_MAX_LENGTH")
authResult, _ := t.authService.checkIfManagerOrAdmin(c, "", Admin)
if !authResult {
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New("Requester must be admin"), http.StatusUnauthorized, nil))
return
}
var addTeamRequest request.AddTeamRequest
if err := c.ShouldBindJSON(&addTeamRequest); err != nil {
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
teamName := addTeamRequest.Name
if &teamName == nil || len(teamName) == 0 {
logger.Error("team name is empty")
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Team name must be between %v - %v", minLength, maxLength)), http.StatusBadRequest, nil))
return
}
if len(teamName) < minLength || len(teamName) > maxLength {
errorMessage := fmt.Sprintf("Team name must be between %v - %v", minLength, maxLength)
logger.Error(errorMessage)
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(errorMessage), http.StatusBadRequest, nil))
return
}
var slackUserList []string
var managerHandle string
managerEmail := addTeamRequest.ManagerEmail
if &managerEmail != nil && len(managerEmail) > 0 {
userInfo, err := t.client.GetUserByEmail(managerEmail)
if isUserInvalid(userInfo, err) {
logger.Error("error in getting on manager info", zap.String("managerEmail", managerEmail), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("Manager handle is invalid"), http.StatusBadRequest, nil))
return
}
managerHandle = userInfo.ID
slackUserList = append(slackUserList, managerHandle)
} else {
logger.Error("Manager email not provided")
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("Manager email not provided"), http.StatusBadRequest, nil))
return
}
oncallHandle := addTeamRequest.OncallHandle
if &oncallHandle != nil && len(oncallHandle) > 0 {
userInfo, err := t.client.GetUsersInfo(addTeamRequest.OncallHandle)
if isUserInvalid(&(*userInfo)[0], err) {
logger.Error("error in getting on call user info", zap.String("userId", addTeamRequest.OncallHandle), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("On call handle is invalid"), http.StatusBadRequest, nil))
return
}
}
for _, userEmail := range addTeamRequest.SlackUserEmails {
if &userEmail != nil && len(userEmail) > 0 && userEmail != managerEmail {
userInfo, err := t.client.GetUserByEmail(userEmail)
if isUserInvalid(userInfo, err) {
logger.Error("error in getting slack user info", zap.String("userEmail", userEmail), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("User email is invalid"), http.StatusBadRequest, nil))
return
}
slackUserList = append(slackUserList, userInfo.ID)
}
}
slackChannel := addTeamRequest.WebhookSlackChannel
if &slackChannel != nil && len(slackChannel) > 0 {
_, err := t.client.GetConversationInfo(addTeamRequest.WebhookSlackChannel)
if err != nil {
logger.Error("error in getting channel info", zap.String("channelId", addTeamRequest.WebhookSlackChannel), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
}
teamEntity := &team.TeamEntity{
Name: addTeamRequest.Name,
SlackUserIds: slackUserList,
OncallHandle: oncallHandle,
Active: true,
WebhookSlackChannel: slackChannel,
ManagerHandle: managerHandle,
CreatedBy: c.GetHeader("X-User-Email"),
UpdatedBy: c.GetHeader("X-User-Email"),
}
_, err := teamRepository.CreateTeam(teamEntity)
if err != nil {
logger.Error("error in creating team", zap.Error(err))
if strings.Contains(err.(*pgconn.PgError).Message, "duplicate key value") {
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("Team name already exists"), http.StatusBadRequest, nil))
} else {
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
}
return
}
c.JSON(http.StatusOK, common.SuccessResponse(service.AddTeamResponse{
ID: teamEntity.ID,
Message: "Team created successfully!",
}, http.StatusOK))
}
func isUserInvalid(userInfo *slack.User, err error) bool {
return err != nil || &userInfo == nil || &(*userInfo).ID == nil || len((*userInfo).ID) == 0
}
func (t *TeamService) GetTeams(c *gin.Context) {
teamId := c.Param("id")
logRepository := log.NewLogRepository(t.db)
teamRepository := team.NewTeamRepository(t.db, logRepository)
if teamId != "" {
TeamId, err := strconv.Atoi(teamId)
if err != nil {
logger.Error("error in parsing teamId", zap.String("teamId", teamId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
team, err := teamRepository.FindTeamById(uint(TeamId))
if err != nil {
logger.Error("error in fetching team by id", zap.Any("TeamId", TeamId))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
var userResponses []service.UserResponse
for _, userId := range team.SlackUserIds {
usersInfo, err := t.client.GetUsersInfo(userId)
if err != nil || len(*usersInfo) == 0 {
logger.Error("error in getting user info", zap.String("userId", userId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
users := *usersInfo
userResponses = append(userResponses, service.UserResponse{
Id: users[0].ID,
Name: users[0].Profile.RealName,
Email: users[0].Profile.Email,
Image: users[0].Profile.Image32,
})
}
channel, err := t.client.GetConversationInfo(team.WebhookSlackChannel)
teamResponse := service.ConvertToTeamResponse(*team)
teamResponse.WebhookSlackChannelId = team.WebhookSlackChannel
if err != nil {
logger.Error("error in getting channel info", zap.String("channelId", team.WebhookSlackChannel), zap.Error(err))
teamResponse.WebhookSlackChannelName = "not found"
} else {
teamResponse.WebhookSlackChannelName = channel.Name
}
oncallBots, err := t.client.GetUsersInfo(team.OncallHandle)
if err != nil || *oncallBots == nil || isUserInvalid(&(*oncallBots)[0], err) {
logger.Error(fmt.Sprintf("error in GetUsersInfo for oncall bot of id: %v", team.OncallHandle))
} else {
oncallBotData := &(*oncallBots)[0]
teamResponse.Oncall = service.BotResponse{
Id: oncallBotData.ID,
Name: oncallBotData.RealName,
}
}
pseOncallBots, err := t.client.GetUsersInfo(team.PseOncallHandle)
if err != nil || *pseOncallBots == nil || isUserInvalid(&(*pseOncallBots)[0], err) {
logger.Error(fmt.Sprintf("error in GetUsersInfo for pse oncall bot of id: %v", team.PseOncallHandle))
} else {
pseOncallBotData := &(*pseOncallBots)[0]
teamResponse.PseOncall = service.BotResponse{
Id: pseOncallBotData.ID,
Name: pseOncallBotData.RealName,
}
}
teamResponse.Participants = userResponses
c.JSON(http.StatusOK, common.SuccessResponse(teamResponse, http.StatusOK))
return
}
teamsEntities, err := teamRepository.GetAllActiveTeams()
if err != nil {
logger.Error("error in getting teams", zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
var teamResponses []service.TeamResponse = []service.TeamResponse{}
for _, team := range *teamsEntities {
teamResponses = append(teamResponses, service.ConvertToTeamResponse(team))
}
c.JSON(http.StatusMultiStatus, common.SuccessResponse(teamResponses, http.StatusOK))
}
func (t *TeamService) UpdateTeam(c *gin.Context) {
logRepository := log.NewLogRepository(t.db)
teamRepository := team.NewTeamRepository(t.db, logRepository)
userEmail := c.GetHeader("X-User-Email")
var updateTeamRequest request.UpdateTeamRequest
if err := c.ShouldBindJSON(&updateTeamRequest); err != nil {
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
logger.Info("update team request received", zap.String("userEmail", userEmail), zap.Any("request", updateTeamRequest))
err := utils.ValidateUpdateTeamRequest(updateTeamRequest)
if err != nil {
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
if err != nil {
logger.Error("error in string to int conversion",
zap.Uint("TeamId", updateTeamRequest.Id), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
teamEntity, err := teamRepository.FindTeamById(updateTeamRequest.Id)
if err != nil || teamEntity == nil {
logger.Error("error in fetching team by id", zap.Uint("TeamId", updateTeamRequest.Id), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Error in fetching team by id: %d", updateTeamRequest.Id)), http.StatusBadRequest, nil))
return
}
authResult, _ := t.authService.checkIfAdminOrTeamMember(c, teamEntity.SlackUserIds)
if !authResult {
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New(fmt.Sprintf("%v is neither an admin nor a member of %v team", userEmail, teamEntity.Name)), http.StatusUnauthorized, nil))
return
}
if !teamEntity.Active {
logger.Error(fmt.Sprintf("Cannot update inactive team: %v with id: %d", teamEntity.Name, teamEntity.ID))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Cannot update inactive team: %v", teamEntity.Name)), http.StatusBadRequest, nil))
return
}
slackUserIds := updateTeamRequest.SlackUserIds
for _, ind := range updateTeamRequest.WorkEmailIds {
slackUser, err := t.client.GetUserByEmail(ind)
if err != nil {
logger.Error("error in GetUserByEmail for team", zap.String("workEmail", ind), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Cannot find user with email: %v", ind)), http.StatusBadRequest, nil))
return
} else {
slackUserIds = append(slackUserIds, slackUser.ID)
}
}
slackUserIds = append(slackUserIds, teamEntity.SlackUserIds...)
slackUserIds = commonutil.RemoveDuplicateStr(slackUserIds)
teamEntity.SlackUserIds = slackUserIds
slackChannelId := updateTeamRequest.WebhookSlackChannel
if len(slackChannelId) != 0 {
_, err := t.client.GetConversationInfo(slackChannelId)
if err != nil {
logger.Error("error in GetConversationInfo for channel", zap.String("channelId", slackChannelId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("Unable to find Slack channel with the provided I.D. Please verify the I.D. and re-submit."), http.StatusBadRequest, nil))
return
}
teamEntity.WebhookSlackChannel = slackChannelId
}
pseOnCallId := updateTeamRequest.PseOnCallId
if len(pseOnCallId) != 0 {
if strings.Contains(pseOnCallId, "@navi.com") {
slackUser, err := t.client.GetUserByEmail(pseOnCallId)
if err != nil {
logger.Error(fmt.Sprintf("error in GetUserByEmail for email: %v", pseOnCallId), zap.String("workEmail", pseOnCallId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Cannot find pse oncall user with email: %v", pseOnCallId)), http.StatusBadRequest, nil))
return
} else {
teamEntity.PseOncallHandle = slackUser.ID
}
} else {
slackUser, err := t.client.GetUsersInfo(pseOnCallId)
if err != nil || *slackUser == nil || isUserInvalid(&(*slackUser)[0], err) {
logger.Error(fmt.Sprintf("error in GetUsersInfo for id: %v", pseOnCallId))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Cannot find pse oncall user with id: %v", pseOnCallId)), http.StatusBadRequest, nil))
return
} else {
teamEntity.PseOncallHandle = (*slackUser)[0].ID
}
}
}
onCallHandle := updateTeamRequest.OnCallHandle
if len(onCallHandle) != 0 {
slackUser, err := t.client.GetUsersInfo(onCallHandle)
if err != nil || *slackUser == nil || isUserInvalid(&(*slackUser)[0], err) {
logger.Error(fmt.Sprintf("error in GetUsersInfo for id: %v", onCallHandle))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Cannot find oncall user with id: %v", onCallHandle)), http.StatusBadRequest, nil))
return
} else {
teamEntity.OncallHandle = (*slackUser)[0].ID
}
}
teamEntity.UpdatedBy = userEmail
teamRepository.UpdateTeam(teamEntity)
c.JSON(http.StatusMultiStatus, common.SuccessResponse("Team updated successfully", http.StatusOK))
}
func (t *TeamService) RemoveTeamMember(c *gin.Context) {
userEmail := c.GetHeader("X-User-Email")
teamId := c.Param("id")
slackUserId := c.Param("userId")
logRepository := log.NewLogRepository(t.db)
teamRepository := team.NewTeamRepository(t.db, logRepository)
TeamId, err := utils.ValidateIdParameter(teamId)
if err != nil {
logger.Error(err.Error(), zap.String("teamId", teamId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
teamEntity, err := teamRepository.FindActiveTeamById(TeamId)
if err != nil {
logger.Error("error in fetching team by id", zap.Any("TeamId", TeamId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New(fmt.Sprintf("Error in fetching team by id : %v", teamId)), http.StatusBadRequest, nil))
return
}
if teamEntity == nil {
err := errors.New(fmt.Sprintf("team with given team id : %v is not found", teamId))
logger.Debug("team with given team id is not found", zap.Any("TeamId", TeamId))
c.JSON(http.StatusNotFound, common.ErrorResponse(err, http.StatusNotFound, nil))
return
}
authResult, _ := t.authService.checkIfManagerOrAdmin(c, teamEntity.ManagerHandle, Admin, Manager)
if !authResult {
err := errors.New(fmt.Sprintf("%v is neither an admin nor the manager of %v team", userEmail, teamEntity.Name))
c.JSON(http.StatusUnauthorized, common.ErrorResponse(err, http.StatusUnauthorized, nil))
return
}
if slackUserId == teamEntity.ManagerHandle {
logger.Debug("manager cannot be removed", zap.Any("TeamId", TeamId), zap.Any("slackId", slackUserId))
c.JSON(http.StatusBadRequest, common.ErrorResponse(errors.New("manager cannot be removed"), http.StatusBadRequest, nil))
return
}
for indexToDelete, userId := range teamEntity.SlackUserIds {
if userId == slackUserId {
teamEntity.SlackUserIds = append(teamEntity.SlackUserIds[:indexToDelete], teamEntity.SlackUserIds[indexToDelete+1:]...)
teamEntity.UpdatedBy = houstonSlackUtil.GetSlackUserIdFromEmail(userEmail, t.client)
err = teamRepository.UpdateTeam(teamEntity)
if err != nil {
logger.Error("error in removing slack user id and saving to database", zap.Any("TeamId", TeamId), zap.Any("slackId", slackUserId), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse("Team member removed successfully", http.StatusOK))
return
}
}
c.JSON(http.StatusBadRequest, common.SuccessResponse(fmt.Sprintf("invalid team member %v", slackUserId), http.StatusBadRequest))
}
func (t *TeamService) MakeManager(c *gin.Context) {
userEmail := c.GetHeader("X-User-Email")
teamId := c.Param("id")
teamMemberToMakeManager := c.Param("userId")
logRepository := log.NewLogRepository(t.db)
teamRepository := team.NewTeamRepository(t.db, logRepository)
TeamId, err := utils.ValidateIdParameter(teamId)
if err != nil {
logger.Error("error reading team id", zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
teamEntity, err := teamRepository.FindActiveTeamById(TeamId)
if err != nil {
logger.Error(fmt.Sprintf("error in fetching team with id: %v", teamId), zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
if teamEntity == nil {
err := errors.New(fmt.Sprintf("team with id: %v not found", teamId))
c.JSON(http.StatusNotFound, common.ErrorResponse(err, http.StatusNotFound, nil))
return
}
authResult, _ := t.authService.checkIfManagerOrAdmin(c, teamEntity.ManagerHandle, Admin, Manager)
if !authResult {
err := errors.New(fmt.Sprintf("%v is neither an admin nor the manager of %v team", userEmail, teamEntity.Name))
c.JSON(http.StatusUnauthorized, common.ErrorResponse(err, http.StatusUnauthorized, nil))
return
}
for _, userId := range teamEntity.SlackUserIds {
if userId == teamMemberToMakeManager {
teamEntity.ManagerHandle = teamMemberToMakeManager
teamEntity.UpdatedBy = houstonSlackUtil.GetSlackUserIdFromEmail(userEmail, t.client)
err = teamRepository.UpdateTeam(teamEntity)
if err != nil {
logger.Error(fmt.Sprintf("error updating %v as manager of %v", teamMemberToMakeManager, teamEntity.Name), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
c.JSON(http.StatusOK, common.SuccessResponse("Team manager updated successfully", http.StatusOK))
return
}
}
c.JSON(http.StatusNotFound, common.ErrorResponse(errors.New(fmt.Sprintf("team member with id: %v not found in %v", teamMemberToMakeManager, teamEntity.Name)), http.StatusNotFound, nil))
}
func (t *TeamService) RemoveTeam(c *gin.Context) {
userEmail := c.GetHeader("X-User-Email")
teamId := c.Param("id")
authResult, _ := t.authService.checkIfManagerOrAdmin(c, "", Admin)
if !authResult {
logger.Error("error in removing team", zap.Error(errors.New(fmt.Sprintf("%v is not an admin", userEmail))))
c.JSON(http.StatusUnauthorized, common.ErrorResponse(errors.New(fmt.Sprintf("%v is not an admin", userEmail)), http.StatusUnauthorized, nil))
return
}
logRepository := log.NewLogRepository(t.db)
teamRepository := team.NewTeamRepository(t.db, logRepository)
TeamId, err := utils.ValidateIdParameter(teamId)
if err != nil {
logger.Error("error reading team id", zap.Error(err))
c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil))
return
}
teamEntity, err := teamRepository.FindActiveTeamById(TeamId)
if err != nil {
logger.Error(fmt.Sprintf("error in fetching team with id: %v", teamId), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
if teamEntity == nil {
err := errors.New(fmt.Sprintf("team with id: %v not found", teamId))
c.JSON(http.StatusNotFound, common.ErrorResponse(err, http.StatusNotFound, nil))
return
}
teamEntity.Active = false
teamEntity.UpdatedBy = houstonSlackUtil.GetSlackUserIdFromEmail(userEmail, t.client)
err = teamRepository.UpdateTeamStatus(teamEntity)
if err != nil {
logger.Error(fmt.Sprintf("error in deleting team with id: %v", teamId), zap.Error(err))
c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil))
return
}
logger.Info(fmt.Sprintf("team with id: %v deleted successfully", teamId))
c.JSON(http.StatusOK, common.SuccessResponse("Team deleted successfully", http.StatusOK))
}