From 233c632d38c00f9a6a8deb060c84674803280c48 Mon Sep 17 00:00:00 2001 From: Shashank Shekhar Date: Tue, 19 Mar 2024 16:26:30 +0530 Subject: [PATCH] INFRA-2866 | Create and update incident with assigner and responder from slack (#394) * INFRA-2866 | create incident modal with product * INFRA-2866 | Update product flow * INFRA-2866 | Resolving review comments * INFRA-2866 | Adding default values for product, assigner and responder * INFRA-2866 | bug fix in getting assigner and responder team * INFRA-2866 | bug-fix: users in no team are not getting products * INFRA-2866 | adding log lines * INFRA-2866 | adding assigner team members into incident * INFRA-2866 | updated help command response text * INFRA-2866 | adding assigner team members by severity * INFRA-2866 | updating product list for users with no product * INFRA-2866 | assigner teams = (teamsOfUser ++ teamsOfSelectedProducts) * INFRA-2866 | renamed assigner to reporting team * INFRA-2866 | query to seed product as others for current open incidents without any product --- cmd/app/handler/incident_handler.go | 20 +- cmd/app/handler/slack_handler.go | 39 +- cmd/app/server.go | 19 +- common/util/common_util.go | 41 ++ common/util/constant.go | 33 +- common/util/slack/slack_helpers.go | 8 +- common/util/string/string_util.go | 14 + config/application.properties | 4 +- ...er_team_column_into_incident_entity.up.sql | 12 + .../incident_channel_message_update_action.go | 12 +- .../action/incident_update_product_action.go | 192 +++++++++ .../action/incident_update_severity_action.go | 5 +- .../action/incident_update_status_action.go | 4 +- .../action/incident_update_type_action.go | 65 +-- .../processor/action/member_join_action.go | 14 +- ...open_set_team_view_modal_command_action.go | 22 +- .../action/select_product_block_action.go | 53 +++ .../action/start_incident_block_action.go | 102 ++++- .../action/start_incident_command_action.go | 152 ++----- .../start_incident_modal_submission_action.go | 91 +++- .../action/view/create_incident_modal.go | 71 +++- .../processor/action/view/incident_section.go | 13 +- .../action/view/incident_summary_section.go | 57 ++- .../action/view/incident_update_product.go | 69 +++ ...e.go => incident_update_responder_team.go} | 22 +- .../action/view/select_product_modal.go | 62 +++ .../event_type_interactive_processor.go | 75 +++- ...n_set_team_view_modal_command_processor.go | 4 +- .../start_incident_command_processor.go | 9 +- internal/resolver/houston_command_resolver.go | 23 +- internal/slack_private_metadata.go | 74 ++++ model/incident/entity.go | 74 ++-- model/incident/incident.go | 31 +- model/incident/model.go | 73 ++-- model/incident_channel/incident_channel.go | 4 +- .../products_teams_repository_impl.go | 14 +- model/team/entity.go | 4 +- model/teamSeverity/entity.go | 2 +- model/teamUser/entity.go | 2 +- .../teamUser/team_user_repository_impl.go | 2 +- .../incident/impl/incident_service_test.go | 65 +-- service/incident/impl/incident_service_v2.go | 402 +++++++++++++----- .../incident/incident_service_v2_interface.go | 4 +- .../incident_channel_service.go | 2 +- .../incident_channel_service_interface.go | 2 +- service/incident_service.go | 81 +++- .../orchestration/incident_orchestrator.go | 10 +- .../incident_orchestrator_impl.go | 219 +++++++--- .../incident_orchestrator_test.go | 43 +- service/products/product_service.go | 1 + service/products/product_service_impl.go | 15 + .../productsTeams/products_teams_service.go | 4 +- .../products_teams_service_impl.go | 40 +- service/request/incident/create_incident.go | 2 +- service/request/update_incident.go | 16 +- service/response/incident_response.go | 76 ++-- .../incidnt_orchestration_response.go | 77 +++- service/utils/validations.go | 11 +- 58 files changed, 1982 insertions(+), 675 deletions(-) create mode 100644 db/migration/000020_add_assigner_team_column_into_incident_entity.up.sql create mode 100644 internal/processor/action/incident_update_product_action.go create mode 100644 internal/processor/action/select_product_block_action.go create mode 100644 internal/processor/action/view/incident_update_product.go rename internal/processor/action/view/{incident_update_type.go => incident_update_responder_team.go} (54%) create mode 100644 internal/processor/action/view/select_product_modal.go create mode 100644 internal/slack_private_metadata.go diff --git a/cmd/app/handler/incident_handler.go b/cmd/app/handler/incident_handler.go index 7b2692a..7a25b9e 100644 --- a/cmd/app/handler/incident_handler.go +++ b/cmd/app/handler/incident_handler.go @@ -203,25 +203,25 @@ func (handler *IncidentHandler) HandleGetJiraStatuses(c *gin.Context) { func (handler *IncidentHandler) HandleGetProductsOfUser(c *gin.Context) { emailID := c.GetHeader(util.UserEmailHeader) - products, err := handler.orchestrator.GetProductsOfUser(emailID) + products, err := handler.orchestrator.GetProductsOfUserByEmailID(emailID) if err != nil { - c.JSON(http.StatusInternalServerError, err) + c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) } - c.JSON(http.StatusOK, products) + c.JSON(http.StatusOK, common.SuccessResponse(products.ToResponse(), http.StatusOK)) } -func (handler *IncidentHandler) HandleGetAssignerAndResponderTeams(c *gin.Context) { +func (handler *IncidentHandler) HandleGetReportingAndResponderTeams(c *gin.Context) { emailID := c.GetHeader(util.UserEmailHeader) - productIDStr := c.Param("id") - productID, err := stringUtils.StringToUint(productIDStr) + productIDsAsStringArray := c.QueryArray("productID") + productIDs, err := stringUtils.StringArrayToUintArray(productIDsAsStringArray) if err != nil { - c.JSON(http.StatusBadRequest, fmt.Errorf("invalid product id: %s", productIDStr)) + c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) return } - assignerAndResponderTeams, err := handler.orchestrator.GetAssignerAndResponderTeams(emailID, productID) + reportingAndResponderTeams, err := handler.orchestrator.GetReportingAndResponderTeams(emailID, productIDs) if err != nil { - c.JSON(http.StatusInternalServerError, err) + c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) } - c.JSON(http.StatusOK, assignerAndResponderTeams) + c.JSON(http.StatusOK, common.SuccessResponse(reportingAndResponderTeams.ToResponse(), http.StatusOK)) } diff --git a/cmd/app/handler/slack_handler.go b/cmd/app/handler/slack_handler.go index 07fe12b..025e7d2 100644 --- a/cmd/app/handler/slack_handler.go +++ b/cmd/app/handler/slack_handler.go @@ -21,6 +21,7 @@ import ( "houston/repository/rcaInput" "houston/service/documentService" incidentServiceV2 "houston/service/incident/impl" + "houston/service/orchestration" rcaService "houston/service/rca/impl" slack2 "houston/service/slack" @@ -44,12 +45,15 @@ type slackHandler struct { houstonCommandResolver *resolver.HoustonCommandResolver } -func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) *slackHandler { +func NewSlackHandler( + gormClient *gorm.DB, socketModeClient *socketmode.Client, orchestrator orchestration.IncidentOrchestrator, +) *slackHandler { severityService := severity.NewSeverityRepository(gormClient) logRepository := log.NewLogRepository(gormClient) tagService := tag.NewTagRepository(gormClient) teamService := team.NewTeamRepository(gormClient, logRepository) incidentService := incident.NewIncidentRepository(gormClient, severityService, logRepository, teamService, socketModeClient) + productsService := appcontext.GetProductsService() userService := user.NewUserRepository(gormClient) shedlockService := shedlock.NewShedlockRepository(gormClient) slackbotClient := slackbot.NewSlackClient(socketModeClient) @@ -64,6 +68,7 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) * incidentServiceV2, slackService, documentService, rcaRepository, rcaInputRepository, userService, appcontext.GetDriveService(), ) + productTeamService := appcontext.GetProductTeamsService() slashCommandProcessor := processor.NewSlashCommandProcessor(socketModeClient, slackbotClient, rcaService) if viper.GetString("env") != "local" { @@ -87,15 +92,39 @@ func NewSlackHandler(gormClient *gorm.DB, socketModeClient *socketmode.Client) * socketModeClient, incidentService, teamService, severityService, ), blockActionProcessor: processor.NewBlockActionProcessor( - socketModeClient, incidentService, teamService, severityService, tagService, slackbotClient, incidentServiceV2, slackService, rcaService, + socketModeClient, + incidentService, + teamService, + severityService, + tagService, + slackbotClient, + incidentServiceV2, + slackService, rcaService, + productsService, + productTeamService, + orchestrator, ), viewSubmissionProcessor: processor.NewViewSubmissionProcessor( - socketModeClient, incidentService, teamService, severityService, tagService, teamService, slackbotClient, gormClient, - rcaService, incidentServiceV2), + socketModeClient, + incidentService, + productsService, + productTeamService, + teamService, + severityService, + tagService, + teamService, + slackbotClient, + gormClient, + rcaService, + incidentServiceV2, + orchestrator, + ), userChangeEventProcessor: processor.NewUserChangeEventProcessor( socketModeClient, userService, ), - houstonCommandResolver: resolver.NewHoustonCommandResolver(socketModeClient, slackbotClient, rcaService), + houstonCommandResolver: resolver.NewHoustonCommandResolver( + socketModeClient, slackbotClient, rcaService, productsService, orchestrator, + ), } } diff --git a/cmd/app/server.go b/cmd/app/server.go index 0e2e23f..6d9c1d5 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -73,8 +73,19 @@ func (s *Server) Handler(houstonGroup *gin.RouterGroup) { } func (s *Server) houstonHandler() { + incidentServiceV2 := incidentService.NewIncidentServiceV2(s.db) + incidentOrchestrator := orchestration.NewIncidentOrchestrator( + appcontext.GetProductTeamsService(), + appcontext.GetProductsService(), + appcontext.GetTeamUserService(), + appcontext.GetUserService(), + appcontext.GetTeamService(), + appcontext.GetSeverityService(), + appcontext.GetSlackService(), + incidentServiceV2, + ) houstonClient := NewHoustonClient() - houstonHandler := handler.NewSlackHandler(s.db, houstonClient.socketModeClient) + houstonHandler := handler.NewSlackHandler(s.db, houstonClient.socketModeClient, incidentOrchestrator) houstonHandler.HoustonConnect() } @@ -138,7 +149,7 @@ func (s *Server) severityHandler(houstonGroup *gin.RouterGroup) { func (s *Server) incidentClientHandler(houstonGroup *gin.RouterGroup) { houstonClient := NewHoustonClient() - incidentHandler := service.NewIncidentService(s.gin, s.db, houstonClient.socketModeClient) + incidentHandler := service.NewIncidentService(s.gin, s.db, houstonClient.socketModeClient, appcontext.GetTeamService()) // Add a header to the routes in houstonGroup houstonGroup.Use(func(c *gin.Context) { // Add your desired header key-value pair @@ -205,12 +216,12 @@ func (s *Server) incidentClientHandlerV2(houstonGroup *gin.RouterGroup) { houstonGroup.POST("/unlink-jira-from-incident", incidentHandler.HandleJiraUnLinking) houstonGroup.GET("/get-jira-statuses", incidentHandler.HandleGetJiraStatuses) houstonGroup.GET("/user/products", authService.IfValidHoustonUser(incidentHandler.HandleGetProductsOfUser)) - houstonGroup.GET("/product/:id/assigner-and-responder", authService.IfValidHoustonUser(incidentHandler.HandleGetAssignerAndResponderTeams)) + houstonGroup.GET("/product/reporting-and-responder-teams", incidentHandler.HandleGetReportingAndResponderTeams) } func (s *Server) incidentHandler(houstonGroup *gin.RouterGroup) { houstonClient := NewHoustonClient() - incidentHandler := service.NewIncidentService(s.gin, s.db, houstonClient.socketModeClient) + incidentHandler := service.NewIncidentService(s.gin, s.db, houstonClient.socketModeClient, appcontext.GetTeamService()) //Will be deprecated because they are not using hosuton group s.gin.GET("/incidents", incidentHandler.GetIncidents) diff --git a/common/util/common_util.go b/common/util/common_util.go index 949d728..d4755a9 100644 --- a/common/util/common_util.go +++ b/common/util/common_util.go @@ -14,6 +14,7 @@ import ( "houston/model/severity" "houston/model/team" "math" + "reflect" "strings" "sync" "time" @@ -330,3 +331,43 @@ func AreTwoPqInt32ArraysEqual(arrayA pq.Int32Array, arrayB pq.Int32Array) bool { return true } + +func RemoveDuplicates(slice interface{}, fieldName string) interface{} { + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + panic("RemoveDuplicates: not a slice") + } + + // Create a map to store unique values + seen := make(map[interface{}]struct{}) + elementType := v.Type().Elem() + + // Get the field index by name + fieldIndex := -1 + for i := 0; i < elementType.NumField(); i++ { + if elementType.Field(i).Name == fieldName { + fieldIndex = i + break + } + } + + // If field doesn't exist, panic + if fieldIndex == -1 { + panic("RemoveDuplicates: field not found") + } + + // Create a new slice without duplicates + resultSlice := reflect.MakeSlice(reflect.SliceOf(elementType), 0, v.Len()) + + for i := 0; i < v.Len(); i++ { + fieldValue := v.Index(i).Field(fieldIndex).Interface() + + // If value is not seen before, add it to the result slice and mark it as seen + if _, ok := seen[fieldValue]; !ok { + resultSlice = reflect.Append(resultSlice, v.Index(i)) + seen[fieldValue] = struct{}{} + } + } + + return resultSlice.Interface() +} diff --git a/common/util/constant.go b/common/util/constant.go index 00531ec..9558134 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -4,13 +4,15 @@ type BlockActionType string const ( StartIncident BlockActionType = "start_incident" + SelectProduct = "select_product" ShowIncidents = "show_incidents" HelpCommand = "help_button" Incident = "incident" AssignIncidentRole = "assign_incident_role" ResolveIncident = "resolve_incident" SetIncidentStatus = "set_incident_status" - SetIncidentType = "set_incident_type" + SetProduct = "set_product" + SetResponderTeam = "set_responder_team" SetIncidentSeverity = "set_incident_severity" SetIncidentTitle = "set_incident_title" SetIncidentDescription = "set_incident_description" @@ -27,19 +29,22 @@ const ( type ViewSubmissionType string const ( - StartIncidentSubmit ViewSubmissionType = "start_incident_submit" - AssignIncidentRoleSubmit = "assign_incident_role_submit" - SetIncidentStatusSubmit = "set_incident_status_submit" - SetIncidentTitleSubmit = "set_incident_title_submit" - SetIncidentDescriptionSubmit = "set_incident_description_submit" - SetIncidentSeveritySubmit = "set_incident_severity_submit" - SeverityJustificationSubmit = "severity_justification_submit" - SetIncidentTypeSubmit = "set_incident_type_submit" - SetIncidentRCADetailsSubmit = "set_rca_details_submit" - IncidentResolveSubmit = "resolve_incident_submit" - SetIncidentJiraLinksSubmit = "set_Jira_links_submit" - ShowIncidentSubmit = "show_incident_submit" - MarkIncidentDuplicateSubmit = "mark_incident_duplicate_submit" + StartIncidentSubmit ViewSubmissionType = "start_incident_submit" + SelectProductSubmit = "select_product_submit" + AssignIncidentRoleSubmit = "assign_incident_role_submit" + SetIncidentStatusSubmit = "set_incident_status_submit" + SetIncidentTitleSubmit = "set_incident_title_submit" + SetIncidentDescriptionSubmit = "set_incident_description_submit" + SetIncidentSeveritySubmit = "set_incident_severity_submit" + SeverityJustificationSubmit = "severity_justification_submit" + IncidentUpdateProductSelectionSubmit = "incident_update_product_selection_submit" + SetIncidentProductSubmit = "set_incident_product_submit" + SetIncidentTypeSubmit = "set_incident_type_submit" + SetIncidentRCADetailsSubmit = "set_rca_details_submit" + IncidentResolveSubmit = "resolve_incident_submit" + SetIncidentJiraLinksSubmit = "set_Jira_links_submit" + ShowIncidentSubmit = "show_incident_submit" + MarkIncidentDuplicateSubmit = "mark_incident_duplicate_submit" ) const ( diff --git a/common/util/slack/slack_helpers.go b/common/util/slack/slack_helpers.go index a2d1c08..b628c5c 100644 --- a/common/util/slack/slack_helpers.go +++ b/common/util/slack/slack_helpers.go @@ -161,11 +161,11 @@ func GetMultiValueArrayFromSlackOptionBlock(options []slack.OptionBlockObject) ( return arrayValues, nil } -func BuildSlackTextMessageFromMetaData(metaData []byte, isCodeBlock bool) (slack.MsgOption, error) { +func BuildSlackTextMessageFromMetaData(metadata []byte, isCodeBlock bool) (slack.MsgOption, error) { var m []incidentRequest.CreateIncidentMetadata - err := json.Unmarshal(metaData, &m) + err := json.Unmarshal(metadata, &m) if err != nil { logger.Error("Error while unmarshalling metadata", zap.Error(err)) @@ -210,8 +210,8 @@ func BuildSlackTextMessageFromMetaData(metaData []byte, isCodeBlock bool) (slack return slack.MsgOptionText(textMessage, false), nil } -func PostIncidentCustomerDataUpdateMessage(metadata incidentRequest.CreateIncidentMetadata, userId, channelId string, client *socketmode.Client) error { - marshalledMetadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{metadata}) +func PostIncidentCustomerDataUpdateMessage(metadata *incidentRequest.CreateIncidentMetadata, userId, channelId string, client *socketmode.Client) error { + marshalledMetadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*metadata}) msgOption, err := BuildSlackTextMessageFromMetaData(marshalledMetadata, true) if err != nil { return err diff --git a/common/util/string/string_util.go b/common/util/string/string_util.go index 0b7d54f..66a834e 100644 --- a/common/util/string/string_util.go +++ b/common/util/string/string_util.go @@ -31,3 +31,17 @@ func StringToUint(input string) (uint, error) { } return uint(u64), nil } + +func StringArrayToUintArray(strArray []string) ([]uint, error) { + uintArray := make([]uint, len(strArray)) + + for i, str := range strArray { + val, err := StringToUint(str) + if err != nil { + return nil, err + } + uintArray[i] = val + } + + return uintArray, nil +} diff --git a/config/application.properties b/config/application.properties index a7504c7..cabded5 100644 --- a/config/application.properties +++ b/config/application.properties @@ -89,8 +89,8 @@ get-teams.v2.enabled=GET_TEAMS_V2_ENABLED slack.workspace.id=SLACK_WORKSPACE_ID navi.jira.base.url=https://navihq.atlassian.net/browse/ -houston.channel.help.message=/houston: General command to open the other options| /houston severity: Opens the view to update severity of the incident| /houston set severity to : Sets the incident severity| /houston team: Opens the view to update team| /houston set team to : Sets the incident team| /houston status: Opens the view to set status| /houston set status to : Sets the incident status| /houston description: Opens the view to set incident description| /houston set description to : Sets the incident description| /houston resolve: Opens the view to fill RCA and resolve| /houston rca: Opens the view to fill RCA -non.houston.channel.help.message=/houston: General command to open the other options| /houston start: Opens the view to start a new incident| /houston start title description : Starts an incident of a specific severity and team +houston.channel.help.message=/houston: General command to open the other options| /houston severity: Opens the view to update severity of the incident| /houston set severity to : Sets the incident severity| /houston team: Opens the view to update team| /houston status: Opens the view to set status| /houston set status to : Sets the incident status| /houston description: Opens the view to set incident description| /houston set description to : Sets the incident description| /houston resolve: Opens the view to fill RCA and resolve| /houston rca: Opens the view to fill RCA +non.houston.channel.help.message=/houston: General command to open the other options| /houston start: Opens the view to start a new incident jira.base.url=JIRA_BASE_URL jira.username=JIRA_USERNAME diff --git a/db/migration/000020_add_assigner_team_column_into_incident_entity.up.sql b/db/migration/000020_add_assigner_team_column_into_incident_entity.up.sql new file mode 100644 index 0000000..d869999 --- /dev/null +++ b/db/migration/000020_add_assigner_team_column_into_incident_entity.up.sql @@ -0,0 +1,12 @@ +ALTER TABLE incident ADD COLUMN IF NOT EXISTS reporting_team_id bigint REFERENCES team(id); + +INSERT INTO incident_products (incident_entity_id, product_entity_id) +SELECT i.ID as incident_entity_id, 9 as product_entity_id +FROM incident i +WHERE i.status IN (1, 2, 3) + AND NOT EXISTS ( + SELECT 1 + FROM incident_products ip + WHERE ip.incident_entity_id = i.ID + AND ip.product_entity_id = 9 +); diff --git a/internal/processor/action/incident_channel_message_update_action.go b/internal/processor/action/incident_channel_message_update_action.go index 587b8fa..27ad73d 100644 --- a/internal/processor/action/incident_channel_message_update_action.go +++ b/internal/processor/action/incident_channel_message_update_action.go @@ -41,8 +41,18 @@ func (icm *IncidentChannelMessageUpdateAction) ProcessAction(channelId string) { if err != nil { return } + var reportingTeamEntity *team.TeamEntity = nil + if incidentEntity.ReportingTeamId != nil { + var err error + reportingTeamEntity, err = icm.teamService.FindTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("failed to get reporting team"), zap.Error(err)) + } + } - blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + blocks := view.IncidentSummarySectionV3( + incidentEntity, reportingTeamEntity, teamEntity, severityEntity, incidentStatusEntity, + ) color := util.GetColorBySeverity(severityEntity.ID) att := slack.Attachment{Blocks: blocks, Color: color} for _, message := range *incidentChannels { diff --git a/internal/processor/action/incident_update_product_action.go b/internal/processor/action/incident_update_product_action.go new file mode 100644 index 0000000..73bc09b --- /dev/null +++ b/internal/processor/action/incident_update_product_action.go @@ -0,0 +1,192 @@ +package action + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/common/util" + stringUtil "houston/common/util/string" + "houston/internal" + "houston/internal/processor/action/view" + "houston/logger" + "houston/model/incident" + "houston/model/severity" + "houston/pkg/slackbot" + incidentV2 "houston/service/incident/impl" + "houston/service/orchestration" + "houston/service/products" + "houston/service/productsTeams" + service "houston/service/request" + "strconv" + "time" +) + +type IncidentUpdateProductAction struct { + socketModeClient *socketmode.Client + productService products.ProductService + productTeamService productsTeams.ProductTeamsService + incidentRepository *incident.Repository + severityRepository *severity.Repository + slackbotClient *slackbot.Client + incidentServiceV2 *incidentV2.IncidentServiceV2 + incidentOrchestrator orchestration.IncidentOrchestrator +} + +func NewIncidentUpdateProductAction( + client *socketmode.Client, + incidentService *incident.Repository, + productService products.ProductService, + productTeamService productsTeams.ProductTeamsService, + severityService *severity.Repository, + slackbotClient *slackbot.Client, + incidentServiceV2 *incidentV2.IncidentServiceV2, + incidentOrchestrator orchestration.IncidentOrchestrator, +) *IncidentUpdateProductAction { + return &IncidentUpdateProductAction{ + socketModeClient: client, + productService: productService, + productTeamService: productTeamService, + incidentRepository: incidentService, + severityRepository: severityService, + slackbotClient: slackbotClient, + incidentServiceV2: incidentServiceV2, + incidentOrchestrator: incidentOrchestrator, + } +} + +func (action *IncidentUpdateProductAction) IncidentSetProductRequestProcess( + callback slack.InteractionCallback, request *socketmode.Request, +) { + var channelID = callback.Channel.ID + logger.Info("[IncidentSetProductRequestProcess] channel id is: " + channelID) + productsForUpdate := action.incidentOrchestrator.GetProductsForUpdate(channelID) + if productsForUpdate == nil { + logger.Error("error in fetching products for incident update") + return + } + + modalRequest := view.BuildIncidentUpdateProductSelectionModal(channelID, productsForUpdate) + + _, err := action.socketModeClient.OpenView(callback.TriggerID, modalRequest) + if err != nil { + logger.Error("houston slackbot openview command failed.", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", channelID), zap.Error(err)) + return + } + var payload interface{} + action.socketModeClient.Ack(*request, payload) +} + +func (action *IncidentUpdateProductAction) IncidentUpdateProductSelectionRequestProcess( + callback slack.InteractionCallback, request *socketmode.Request, +) { + channelID := callback.View.PrivateMetadata + var payload interface{} + action.socketModeClient.Ack(*request, payload) + + productIDs := action.getProductIds(callback.View.State.Values) + + logger.Info("[IncidentUpdateProductSelectionRequestProcess] channel id is: " + channelID) + + responderTeams := action.incidentOrchestrator.GetResponderTeamsForUpdate(channelID, productIDs) + + modalRequest := view.BuildIncidentUpdateResponderTeamModal(channelID, responderTeams) + slackPrivateMetadata := internal.NewSlackPrivateMetadata(). + SetChannel(callback.View.PrivateMetadata). + SetTriggerId(callback.TriggerID). + SetProductIds(productIDs) + modalRequest.PrivateMetadata = slackPrivateMetadata.String() + modalRequest.CallbackID = util.SetIncidentProductSubmit + time.Sleep(500 * time.Millisecond) + _, err := action.socketModeClient.OpenView(callback.TriggerID, modalRequest) + if err != nil { + logger.Error("houston slackbot openview command failed.", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) + return + } +} + +func (action *IncidentUpdateProductAction) IncidentUpdateProductRequestProcess( + callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User, +) { + slackPrivateMetadata, err := internal.StringToSlackPrivateMetadata(callback.View.PrivateMetadata) + if err != nil { + logger.Error("error in getting product ids from slack private metadata", zap.Error(err)) + return + } + channelID := slackPrivateMetadata.GetChannel() + incidentEntity, err := action.incidentRepository.FindIncidentByChannelId(channelID) + if err != nil { + logger.Error("FindIncidentByChannelId error", + zap.String("incident_slack_channel_id", channel.ID), zap.String("channel", channel.Name), + zap.String("user_id", user.ID), zap.Error(err)) + return + } else if incidentEntity == nil { + logger.Error("IncidentEntity not found ", + zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), + zap.String("user_id", callback.User.ID), zap.Error(err)) + return + } + + _, severityEntity, incidentStatusEntity, incidentChannels, err := action.incidentServiceV2.FetchAllEntitiesForIncident(incidentEntity) + if err != nil { + logger.Error(fmt.Sprintf("error in fetching entities for incident with id: %d %v", incidentEntity.ID, err)) + return + } + + var payload interface{} + action.socketModeClient.Ack(*request, payload) + productIDs := slackPrivateMetadata.GetProductIds() + responderTeamID := action.getResponderTeamID(callback.View.State.Values) + + if err := action.incidentServiceV2.UpdateProductID( + service.UpdateIncidentRequest{ + Id: incidentEntity.ID, + ProductIDs: productIDs, + TeamId: fmt.Sprintf("%d", responderTeamID), + MetaData: nil, + }, + user.ID, + incidentEntity, + severityEntity, + incidentStatusEntity, + incidentChannels, + ); err != nil { + logger.Error(fmt.Sprintf("error in updating product: %v", err)) + } +} + +func (action *IncidentUpdateProductAction) getProductIds(blockActions map[string]map[string]slack.BlockAction) []uint { + var productIds []uint + for _, actions := range blockActions { + for _, o := range actions["productIds"].SelectedOptions { + productIdValue, err := stringUtil.StringToUint(o.Value) + if err != nil { + logger.Error("[IncidentUpdateProductAction] failed to parse product id", zap.Error(err)) + return nil + } + productIds = append(productIds, productIdValue) + } + } + + return productIds +} + +func (action *IncidentUpdateProductAction) getResponderTeamID(blockActions map[string]map[string]slack.BlockAction) uint { + var requestMap = make(map[string]string) + for _, actions := range blockActions { + for actionID, a := range actions { + if string(a.Type) == slack.OptTypeStatic { + requestMap[actionID] = a.SelectedOption.Value + } + } + } + + selectedValue := requestMap["incident_responder_team_modal_request"] + selectedValueInInt, err := strconv.Atoi(selectedValue) + if err != nil { + logger.Error("String conversion to int failed in buildUpdateIncidentResponderTeamRequest for "+selectedValue, zap.Error(err)) + } + return uint(selectedValueInInt) +} diff --git a/internal/processor/action/incident_update_severity_action.go b/internal/processor/action/incident_update_severity_action.go index e5e0a3f..942606e 100644 --- a/internal/processor/action/incident_update_severity_action.go +++ b/internal/processor/action/incident_update_severity_action.go @@ -17,7 +17,6 @@ import ( "houston/pkg/slackbot" incidentService "houston/service/incident/impl" service "houston/service/request" - incidentRequest "houston/service/request/incident" "strconv" "time" ) @@ -163,7 +162,7 @@ func (isp *IncidentUpdateSevertityAction) IncidentUpdateSeverity(callback slack. service.UpdateIncidentRequest{ Id: incidentEntity.ID, SeverityId: strconv.Itoa(incidentSeverityId), - MetaData: incidentRequest.CreateIncidentMetadata{}, + MetaData: nil, }, user.ID, incidentEntity, @@ -277,7 +276,7 @@ func buildUpdateIncidentSeverityRequest(blockActions map[string]map[string]slack selectedValue := requestMap["incident_severity_modal_request"] selectedValueInInt, err := strconv.Atoi(selectedValue) if err != nil { - logger.Error("String conversion to int faileed in buildUpdateIncidentTypeRequest for "+selectedValue, zap.Error(err)) + logger.Error("String conversion to int faileed in buildUpdateIncidentResponderTeamRequest for "+selectedValue, zap.Error(err)) } return selectedValueInInt } diff --git a/internal/processor/action/incident_update_status_action.go b/internal/processor/action/incident_update_status_action.go index 5e6fe59..274e061 100644 --- a/internal/processor/action/incident_update_status_action.go +++ b/internal/processor/action/incident_update_status_action.go @@ -55,7 +55,7 @@ func (isp *UpdateIncidentAction) IncidentUpdateStatusRequestProcess(callback sla isp.client.Ack(*request, payload) } -//todo: this method has to be replaced with incident-service-v2 UpdateStatus method +// todo: this method has to be replaced with incident-service-v2 UpdateStatus method func (isp *UpdateIncidentAction) IncidentUpdateStatus(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { incidentEntity, err := isp.incidentService.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { @@ -151,7 +151,7 @@ func buildUpdateIncidentStatusRequest(blockActions map[string]map[string]slack.B selectedValue := requestMap["incident_status_modal_request"] selectedValueInInt, err := strconv.Atoi(selectedValue) if err != nil { - logger.Error("String conversion to int faileed in buildUpdateIncidentTypeRequest for "+selectedValue, zap.Error(err)) + logger.Error("String conversion to int faileed in buildUpdateIncidentResponderTeamRequest for "+selectedValue, zap.Error(err)) } return uint(selectedValueInInt) } diff --git a/internal/processor/action/incident_update_type_action.go b/internal/processor/action/incident_update_type_action.go index 00ee31e..93b2ddb 100644 --- a/internal/processor/action/incident_update_type_action.go +++ b/internal/processor/action/incident_update_type_action.go @@ -13,43 +13,46 @@ import ( "houston/model/team" "houston/pkg/slackbot" incidentV2 "houston/service/incident/impl" + "houston/service/orchestration" service "houston/service/request" - incidentRequest "houston/service/request/incident" "strconv" ) type IncidentUpdateTypeAction struct { - socketModeClient *socketmode.Client - teamRepository *team.Repository - incidentRepository *incident.Repository - severityRepository *severity.Repository - slackbotClient *slackbot.Client - incidentServiceV2 *incidentV2.IncidentServiceV2 + socketModeClient *socketmode.Client + teamRepository *team.Repository + incidentRepository *incident.Repository + severityRepository *severity.Repository + slackbotClient *slackbot.Client + incidentServiceV2 *incidentV2.IncidentServiceV2 + incidentOrchestrator orchestration.IncidentOrchestrator } -func NewIncidentUpdateTypeAction(client *socketmode.Client, incidentService *incident.Repository, teamService *team.Repository, severityService *severity.Repository, slackbotClient *slackbot.Client, incidentServiceV2 *incidentV2.IncidentServiceV2) *IncidentUpdateTypeAction { +func NewIncidentUpdateTypeAction( + client *socketmode.Client, + incidentService *incident.Repository, + teamService *team.Repository, + severityService *severity.Repository, + slackbotClient *slackbot.Client, + incidentServiceV2 *incidentV2.IncidentServiceV2, + incidentOrchestrator orchestration.IncidentOrchestrator, +) *IncidentUpdateTypeAction { return &IncidentUpdateTypeAction{ - socketModeClient: client, - teamRepository: teamService, - incidentRepository: incidentService, - severityRepository: severityService, - slackbotClient: slackbotClient, - incidentServiceV2: incidentServiceV2, + socketModeClient: client, + teamRepository: teamService, + incidentRepository: incidentService, + severityRepository: severityService, + slackbotClient: slackbotClient, + incidentServiceV2: incidentServiceV2, + incidentOrchestrator: incidentOrchestrator, } } func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateTypeRequestProcess(callback slack.InteractionCallback, request *socketmode.Request) { - teams, err := incidentUpdateTypeAction.teamRepository.GetAllActiveTeams() - if err != nil { - logger.Error("GetAllActiveTeams error", - zap.String("incident_slack_channel_id", callback.Channel.ID), zap.String("channel", callback.Channel.Name), - zap.String("user_id", callback.User.ID), zap.Error(err)) - return - } + responderTeams := incidentUpdateTypeAction.incidentOrchestrator.GetResponderTeamsForUpdate(callback.Channel.ID, nil) + modalRequest := view.BuildIncidentUpdateResponderTeamModal(callback.Channel.ID, responderTeams) - modalRequest := view.BuildIncidentUpdateTypeModal(callback.Channel.ID, *teams) - - _, err = incidentUpdateTypeAction.socketModeClient.OpenView(callback.TriggerID, modalRequest) + _, err := incidentUpdateTypeAction.socketModeClient.OpenView(callback.TriggerID, modalRequest) if err != nil { logger.Error("houston slackbot openview command failed.", zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) @@ -59,7 +62,9 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateTypeRequ incidentUpdateTypeAction.socketModeClient.Ack(*request, payload) } -func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateType(callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User) { +func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateType( + callback slack.InteractionCallback, request *socketmode.Request, channel slack.Channel, user slack.User, +) { incidentEntity, err := incidentUpdateTypeAction.incidentRepository.FindIncidentByChannelId(callback.View.PrivateMetadata) if err != nil { logger.Error("FindIncidentByChannelId error", @@ -82,13 +87,13 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) IncidentUpdateType(cal var payload interface{} incidentUpdateTypeAction.socketModeClient.Ack(*request, payload) - teamID := incidentUpdateTypeAction.buildUpdateIncidentTypeRequest(callback.View.State.Values) + teamID := incidentUpdateTypeAction.buildUpdateIncidentResponderTeamRequest(callback.View.State.Values) if err := incidentUpdateTypeAction.incidentServiceV2.UpdateTeamId( service.UpdateIncidentRequest{ Id: incidentEntity.ID, TeamId: fmt.Sprintf("%d", teamID), - MetaData: incidentRequest.CreateIncidentMetadata{}, + MetaData: nil, }, user.ID, incidentEntity, @@ -111,7 +116,7 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) addDefaultUsersToIncid return nil } -func (incidentUpdateTypeAction *IncidentUpdateTypeAction) buildUpdateIncidentTypeRequest(blockActions map[string]map[string]slack.BlockAction) uint { +func (incidentUpdateTypeAction *IncidentUpdateTypeAction) buildUpdateIncidentResponderTeamRequest(blockActions map[string]map[string]slack.BlockAction) uint { var requestMap = make(map[string]string, 0) for _, actions := range blockActions { for actionID, a := range actions { @@ -121,10 +126,10 @@ func (incidentUpdateTypeAction *IncidentUpdateTypeAction) buildUpdateIncidentTyp } } - selectedValue := requestMap["incident_type_modal_request"] + selectedValue := requestMap["incident_responder_team_modal_request"] selectedValueInInt, err := strconv.Atoi(selectedValue) if err != nil { - logger.Error("String conversion to int faileed in buildUpdateIncidentTypeRequest for "+selectedValue, zap.Error(err)) + logger.Error("String conversion to int failed in buildUpdateIncidentResponderTeamRequest for "+selectedValue, zap.Error(err)) } return uint(selectedValueInInt) } diff --git a/internal/processor/action/member_join_action.go b/internal/processor/action/member_join_action.go index a18ffa9..5c06875 100644 --- a/internal/processor/action/member_join_action.go +++ b/internal/processor/action/member_join_action.go @@ -1,6 +1,7 @@ package action import ( + "fmt" "houston/internal/processor/action/view" "houston/logger" "houston/model/incident" @@ -75,7 +76,18 @@ func (mp *MemberJoinAction) PerformAction(memberJoinedChannelEvent *slackevents. return } - blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + var reportingTeamEntity *team.TeamEntity = nil + if incidentEntity.ReportingTeamId != nil { + var err error + reportingTeamEntity, err = mp.teamService.FindTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("failed to get reporting team"), zap.Error(err)) + } + } + + blocks := view.IncidentSummarySectionV3( + incidentEntity, reportingTeamEntity, teamEntity, severityEntity, incidentStatusEntity, + ) msgOption := view.IncidentEphemeralMessage(incidentEntity, blocks) _, err = mp.client.PostEphemeral(memberJoinedChannelEvent.Channel, memberJoinedChannelEvent.User, msgOption) diff --git a/internal/processor/action/open_set_team_view_modal_command_action.go b/internal/processor/action/open_set_team_view_modal_command_action.go index 92722f1..2459435 100644 --- a/internal/processor/action/open_set_team_view_modal_command_action.go +++ b/internal/processor/action/open_set_team_view_modal_command_action.go @@ -9,22 +9,26 @@ import ( "houston/internal/processor/action/view" "houston/logger" "houston/pkg/slackbot" + "houston/service/orchestration" ) const openSetTeamViewModalActionLogTag = "[open_set_team_view_modal_command_action]" type OpenSetTeamViewModalCommandAction struct { - socketModeClient *socketmode.Client - slackBot *slackbot.Client + socketModeClient *socketmode.Client + slackBot *slackbot.Client + incidentOrchestrator orchestration.IncidentOrchestrator } func NewOpenSetTeamViewModalCommandAction( socketModeClient *socketmode.Client, slackBot *slackbot.Client, + incidentOrchestrator orchestration.IncidentOrchestrator, ) *OpenSetTeamViewModalCommandAction { return &OpenSetTeamViewModalCommandAction{ - socketModeClient: socketModeClient, - slackBot: slackBot, + socketModeClient: socketModeClient, + slackBot: slackBot, + incidentOrchestrator: incidentOrchestrator, } } @@ -51,12 +55,10 @@ func (action *OpenSetTeamViewModalCommandAction) PerformAction(evt *socketmode.E func (action *OpenSetTeamViewModalCommandAction) openSetTeamViewModal(cmd slack.SlashCommand) error { logger.Info("opening set team view modal") return executeForHoustonChannel(cmd, func() error { - teamEntities, err := appcontext.GetTeamRepo().GetAllActiveTeams() - if err != nil { - logger.Error(fmt.Sprintf("%s failed to fetch all active teams from DB. %+v", openSetTeamViewModalActionLogTag, err)) - return fmt.Errorf("failed to fetch all active teams from DB") - } - _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.BuildIncidentUpdateTypeModal(cmd.ChannelID, *teamEntities)) + responderTeams := action.incidentOrchestrator.GetResponderTeamsForUpdate(cmd.ChannelID, nil) + modalRequest := view.BuildIncidentUpdateResponderTeamModal(cmd.ChannelID, responderTeams) + + _, err := action.socketModeClient.OpenView(cmd.TriggerID, modalRequest) if err != nil { return fmt.Errorf("failed to open set team view modal") } diff --git a/internal/processor/action/select_product_block_action.go b/internal/processor/action/select_product_block_action.go new file mode 100644 index 0000000..7f31153 --- /dev/null +++ b/internal/processor/action/select_product_block_action.go @@ -0,0 +1,53 @@ +package action + +import ( + "houston/internal" + "houston/internal/processor/action/view" + "houston/logger" + "houston/service/orchestration" + "houston/service/products" + + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" +) + +type SelectProductBlockAction struct { + socketModeClient *socketmode.Client + productsService products.ProductService + incidentOrchestrator orchestration.IncidentOrchestrator +} + +func NewSelectProductBlockAction( + client *socketmode.Client, + productService products.ProductService, + incidentOrchestrator orchestration.IncidentOrchestrator, +) *SelectProductBlockAction { + return &SelectProductBlockAction{ + socketModeClient: client, + productsService: productService, + incidentOrchestrator: incidentOrchestrator, + } +} + +func (action *SelectProductBlockAction) ProcessAction(request *socketmode.Request, callback slack.InteractionCallback) { + slackPrivateMetadata := internal.NewSlackPrivateMetadata().SetChannel(callback.Channel.ID).SetTriggerId(callback.TriggerID) + productsOfUser, err := action.incidentOrchestrator.GetProductsOfUserBySlackUserID(callback.User.ID) + if err != nil { + logger.Error("[SelectProductBlockAction] failed while getting all active products") + return + } + + modal := view.SelectProductModal(productsOfUser, slackPrivateMetadata) + + _, err = action.socketModeClient.OpenView(callback.TriggerID, modal) + if err != nil { + logger.Error("[SelectProductBlockAction] houston slackbot open view command failed.", + zap.String("trigger_id", callback.TriggerID), zap.String("channel_id", callback.Channel.ID), zap.Error(err)) + return + } + + logger.Info("[SelectProductBlockAction] houston successfully send modal to slackbot", zap.String("trigger_id", callback.TriggerID)) + var payload interface{} + action.socketModeClient.Ack(*request, payload) +} diff --git a/internal/processor/action/start_incident_block_action.go b/internal/processor/action/start_incident_block_action.go index 778d3d7..cfad177 100644 --- a/internal/processor/action/start_incident_block_action.go +++ b/internal/processor/action/start_incident_block_action.go @@ -1,44 +1,69 @@ package action import ( + "github.com/slack-go/slack" + "github.com/slack-go/slack/socketmode" + "go.uber.org/zap" + "houston/appcontext" + stringUtil "houston/common/util/string" + "houston/internal" "houston/internal/processor/action/view" "houston/logger" "houston/model/severity" "houston/model/team" - - "github.com/slack-go/slack" - "github.com/slack-go/slack/socketmode" - "go.uber.org/zap" + "houston/service/orchestration" + "houston/service/productsTeams" + "time" ) type StartIncidentBlockAction struct { - socketModeClient *socketmode.Client - teamService *team.Repository - severityService *severity.Repository + socketModeClient *socketmode.Client + productTeamService productsTeams.ProductTeamsService + teamService *team.Repository + severityService *severity.Repository + incidentOrchestrator orchestration.IncidentOrchestrator } -func NewStartIncidentBlockAction(client *socketmode.Client, teamService *team.Repository, severityService *severity.Repository) *StartIncidentBlockAction { +func NewStartIncidentBlockAction( + client *socketmode.Client, + teamService *team.Repository, + severityService *severity.Repository, + orchestrator orchestration.IncidentOrchestrator, +) *StartIncidentBlockAction { return &StartIncidentBlockAction{ - socketModeClient: client, - teamService: teamService, - severityService: severityService, + socketModeClient: client, + productTeamService: appcontext.GetProductTeamsService(), + teamService: teamService, + severityService: severityService, + incidentOrchestrator: orchestrator, } } - func (sip *StartIncidentBlockAction) ProcessAction(request *socketmode.Request, callback slack.InteractionCallback) { - teams, err := sip.teamService.GetAllActiveTeams() - if err != nil || teams == nil { - logger.Error("[SIP] failed while getting all active teams") + severities, err := sip.getSeverities() + if err != nil { return } - severities, err := sip.severityService.GetAllActiveSeverity() - if err != nil || severities == nil { - logger.Error("[SIP] failed while getting all active severities") + productIds, productNames := sip.GetProductIds(callback) + reportingAndResponderTeams, err := sip.incidentOrchestrator.GetReportingAndResponderTeamsBySlackUserId(callback.User.ID, productIds) + if err != nil { return } - modal := view.GenerateModalRequest(*teams, *severities, callback.Channel.ID) + privateMetadata, err := internal.StringToSlackPrivateMetadata(callback.View.PrivateMetadata) + if err != nil { + logger.Error("[SIP] failed to parse private metadata", zap.Error(err)) + return + } + + var payload interface{} + sip.socketModeClient.Ack(*request, payload) + + privateMetadata.SetProductIds(productIds).SetProductNames(productNames) + + modal := view.GenerateModalRequest(reportingAndResponderTeams, *severities, privateMetadata) + + time.Sleep(500 * time.Millisecond) _, err = sip.socketModeClient.OpenView(callback.TriggerID, modal) if err != nil { @@ -48,6 +73,41 @@ func (sip *StartIncidentBlockAction) ProcessAction(request *socketmode.Request, } logger.Info("[SIP] houston successfully send modal to slackbot", zap.String("trigger_id", callback.TriggerID)) - var payload interface{} - sip.socketModeClient.Ack(*request, payload) +} + +func (sip *StartIncidentBlockAction) getSeverities() ( + *[]severity.SeverityEntity, error, +) { + var ( + err error + severities *[]severity.SeverityEntity + ) + + severities, err = sip.severityService.GetAllActiveSeverity() + if err != nil || severities == nil { + logger.Error("[SIP] failed while getting all active severities") + return nil, err + } + + return severities, nil +} + +func (sip *StartIncidentBlockAction) GetProductIds(callback slack.InteractionCallback) ([]uint, []string) { + blockActions := callback.View.State.Values + var productIds []uint + var productNames []string + + for _, actions := range blockActions { + for _, o := range actions["productIds"].SelectedOptions { + productIdValue, err := stringUtil.StringToUint(o.Value) + if err != nil { + logger.Error("[StartIncidentBlockAction] failed to parse product id", zap.Error(err)) + return nil, nil + } + productIds = append(productIds, productIdValue) + productNames = append(productNames, o.Text.Text) + } + } + + return productIds, productNames } diff --git a/internal/processor/action/start_incident_command_action.go b/internal/processor/action/start_incident_command_action.go index b6bdacd..c0ca249 100644 --- a/internal/processor/action/start_incident_command_action.go +++ b/internal/processor/action/start_incident_command_action.go @@ -4,35 +4,37 @@ import ( "fmt" "github.com/slack-go/slack" "github.com/slack-go/slack/socketmode" - "github.com/spf13/viper" "go.uber.org/zap" "houston/appcontext" - "houston/common/metrics" - "houston/common/util" "houston/internal" "houston/internal/processor/action/view" "houston/logger" "houston/pkg/slackbot" - incidentService "houston/service/incident/impl" - request "houston/service/request/incident" - "strconv" + "houston/service/orchestration" + "houston/service/products" "strings" ) const startIncidentActionLogTag = "[slash_command_action]" type StartIncidentCommandAction struct { - socketModeClient *socketmode.Client - slackBot *slackbot.Client + productsService products.ProductService + incidentOrchestrator orchestration.IncidentOrchestrator + socketModeClient *socketmode.Client + slackBot *slackbot.Client } func NewStartIncidentCommandAction( + productsService products.ProductService, + incidentOrchestrator orchestration.IncidentOrchestrator, socketModeClient *socketmode.Client, slackBot *slackbot.Client, ) *StartIncidentCommandAction { return &StartIncidentCommandAction{ - socketModeClient: socketModeClient, - slackBot: slackBot, + productsService: productsService, + incidentOrchestrator: incidentOrchestrator, + socketModeClient: socketModeClient, + slackBot: slackBot, } } @@ -64,116 +66,42 @@ func (action *StartIncidentCommandAction) startIncidentWithParams( ) error { logger.Info(fmt.Sprintf("%s received command to start incident with params: %s", startIncidentActionLogTag, params)) return executeForNonHoustonChannel(cmd, func() error { - if len(params) == 0 { - logger.Info(fmt.Sprintf("%s launching view modal to start new incident", startIncidentActionLogTag)) - teams, err := appcontext.GetTeamRepo().GetAllActiveTeams() - if err != nil || teams == nil { - logger.Error(fmt.Sprintf("%s failed to fetch all active teams from DB. %+v", startIncidentActionLogTag, err)) - return genericBackendError - } + if len(params) > 0 { + return fmt.Errorf("invalid command syntax. Run `/houston help` for checking valid syntax") + } - severities, err := appcontext.GetSeverityRepo().GetAllActiveSeverity() - if err != nil || severities == nil { - logger.Error(fmt.Sprintf("%s failed to fetch all severities from DB. %+v", startIncidentActionLogTag, err)) - return genericBackendError - } - _, err = action.socketModeClient.OpenView(cmd.TriggerID, view.GenerateModalRequest(*teams, *severities, cmd.ChannelID)) - if err != nil { - logger.Error(fmt.Sprintf("%s failed to open create new incident view modal: %+v", startIncidentActionLogTag, err)) - return fmt.Errorf("failed to open create new incident view modal") - } - } else { - logger.Info(fmt.Sprintf("%s creating new incident with params: %s", startIncidentActionLogTag, params)) + logger.Info(fmt.Sprintf("%s launching view modal to start new incident", startIncidentActionLogTag)) + teams, err := appcontext.GetTeamRepo().GetAllActiveTeams() + if err != nil || teams == nil { + logger.Error(fmt.Sprintf("%s failed to fetch all active teams from DB. %+v", startIncidentActionLogTag, err)) + return genericBackendError + } - createIncidentRequest, err := buildCreateIncidentRequest(params, cmd.UserID) - if err != nil { - return err - } + severities, err := appcontext.GetSeverityRepo().GetAllActiveSeverity() + if err != nil || severities == nil { + logger.Error(fmt.Sprintf("%s failed to fetch all severities from DB. %+v", startIncidentActionLogTag, err)) + return genericBackendError + } - createIncidentResponse, err := appcontext.GetIncidentService().CreateIncident(*createIncidentRequest, "SLACK", cmd.ChannelID) - if err != nil { - logger.Error(fmt.Sprintf("%s failed to create incident. %+v", startIncidentActionLogTag, err)) - metrics.PublishHoustonFlowFailureMetrics(incidentService.CREATE_INCIDENT, err.Error()) - return fmt.Errorf("failed to create incident") - } - logger.Info(fmt.Sprintf("%s incident created: %+v", startIncidentActionLogTag, createIncidentResponse)) + productsOfUser, err := action.incidentOrchestrator.GetProductsOfUserBySlackUserID(cmd.UserID) + if err != nil { + logger.Error("[SIP] failed while getting all active products") + return err + } + + slackPrivateMetadata := internal.NewSlackPrivateMetadata().SetChannel(cmd.ChannelID).SetTriggerId(cmd.TriggerID) + + modal := view.SelectProductModal(productsOfUser, slackPrivateMetadata) + + _, err = action.socketModeClient.OpenView(cmd.TriggerID, modal) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to open create new incident view modal: %+v", startIncidentActionLogTag, err)) + return fmt.Errorf("failed to open create new incident view modal") } return nil }) } -func buildCreateIncidentRequest(param string, userID string) (*request.CreateIncidentRequestV2, error) { - if !strings.Contains(param, " title ") && strings.Contains(param, " description ") { - return nil, fmt.Errorf("title and description are required. Run `/houston help` for checking valid syntax") - } - firstPart, remainingString := util.SplitUntilWord(param, "title") - severityAndTeamSlice := strings.Split(strings.TrimSpace(firstPart), " ") - if len(severityAndTeamSlice) != 2 { - return nil, fmt.Errorf("enter valid severity and team") - } - - //get valid severity from param - severityName := severityAndTeamSlice[0] - severityEntity, err := appcontext.GetSeverityRepo().FindSeverityByName(severityName) - if err != nil { - logger.Error(fmt.Sprintf("%s error in finding severity entity for severity name: %s. %+v", startIncidentActionLogTag, severityName, err)) - return nil, fmt.Errorf("error in finding severity entity for severity name: %s", severityName) - } - if severityEntity == nil { - return nil, fmt.Errorf("enter a valid severity name") - } - severityID := severityEntity.ID - - //get team from param - teamName := severityAndTeamSlice[1] - teamEntity, err := appcontext.GetTeamRepo().FindTeamByTeamName(teamName) - if err != nil { - logger.Error(fmt.Sprintf("%s error in finding team entity for team name: %s. %+v", startIncidentActionLogTag, teamName, err)) - return nil, fmt.Errorf("error in finding severity entity for severity name: %s", teamName) - } - if teamEntity == nil { - return nil, fmt.Errorf("enter a valid team name") - } - var teamID uint - if teamEntity.Active { - teamID = teamEntity.ID - } else { - return nil, fmt.Errorf("entered team is not active") - } - - //get title and description from param - title, description := util.SplitUntilWord(remainingString, "description") - if len(strings.TrimSpace(title)) == 0 { - return nil, fmt.Errorf("title can not be left empty") - } - - titleMaxLength := viper.GetInt("create-incident.title.max-length") - - if len(strings.TrimSpace(title)) > titleMaxLength { - return nil, fmt.Errorf("title can not be more than %d characters long", titleMaxLength) - } - - if len(strings.TrimSpace(description)) == 0 { - return nil, fmt.Errorf("description can not be left empty") - } - - descriptionMaxLength := viper.GetInt("create-incident.description.max-length") - - if len(strings.TrimSpace(title)) > descriptionMaxLength { - return nil, fmt.Errorf("description can not be more than %d characters long", descriptionMaxLength) - } - - //build request - createIncidentRequest := &request.CreateIncidentRequestV2{ - Title: strings.TrimSpace(title), - Description: strings.TrimSpace(description), - SeverityID: strconv.Itoa(int(severityID)), - TeamID: strconv.Itoa(int(teamID)), - CreatedBy: userID, - } - return createIncidentRequest, nil -} - func executeForNonHoustonChannel(cmd slack.SlashCommand, fn func() error) error { isAHoustonChannel, err := appcontext.GetIncidentService().IsHoustonChannel(cmd.ChannelID) if err != nil { diff --git a/internal/processor/action/start_incident_modal_submission_action.go b/internal/processor/action/start_incident_modal_submission_action.go index a07edc3..b7082c4 100644 --- a/internal/processor/action/start_incident_modal_submission_action.go +++ b/internal/processor/action/start_incident_modal_submission_action.go @@ -7,6 +7,8 @@ import ( "houston/appcontext" "houston/common/metrics" "houston/common/util" + stringUtil "houston/common/util/string" + "houston/internal" "houston/internal/processor/action/view" "houston/logger" "houston/model/incident" @@ -17,6 +19,7 @@ import ( "houston/service/conference" incidentService "houston/service/incident" incidentServiceImpl "houston/service/incident/impl" + "houston/service/orchestration" request "houston/service/request/incident" "strings" "time" @@ -199,6 +202,45 @@ func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessingV2( isp.client.Ack(*request, payload) } +func (isp *CreateIncidentAction) CreateIncidentModalCommandProcessingV3( + callback slack.InteractionCallback, + request *socketmode.Request, +) { + createIncidentRequest, err := buildCreateIncidentRequestV3(callback) + if err != nil { + logger.Error("[CIP] Error in building CreateIncidentRequestV3", zap.Error(err)) + return + } + logger.Info("[CIP] incident request created", zap.Any("request", createIncidentRequest)) + + service := appcontext.GetIncidentService() + orchestrator := orchestration.NewIncidentOrchestrator( + appcontext.GetProductTeamsService(), + appcontext.GetProductsService(), + appcontext.GetTeamUserService(), + appcontext.GetUserService(), + appcontext.GetTeamService(), + appcontext.GetSeverityService(), + appcontext.GetSlackService(), + service, + ) + slackPrivateMetadata, err := internal.StringToSlackPrivateMetadata(callback.View.PrivateMetadata) + if err != nil { + logger.Error("[CIP] failed to parse private metadata", zap.Error(err)) + return + } + _, err = orchestrator.CreateIncident(createIncidentRequest, slackPrivateMetadata.Channel) + if err != nil { + logger.Error("[CIP] Error while creating incident", zap.Error(err)) + metrics.PublishHoustonFlowFailureMetrics(incidentServiceImpl.CREATE_INCIDENT, err.Error()) + return + } + + // Acknowledge the interaction callback + var payload interface{} + isp.client.Ack(*request, payload) +} + func (isp *CreateIncidentAction) addDefaultUsersToIncident(channelId string, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) error { var userIdList []string @@ -214,7 +256,17 @@ func (isp *CreateIncidentAction) postIncidentSummary(blazeGroupChannelID, incide incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity) (*string, error) { // Post incident summary to Blaze Group channel and incident channel - blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + var reportingTeamEntity *team.TeamEntity = nil + if incidentEntity.ReportingTeamId != nil { + var err error + reportingTeamEntity, err = isp.teamRepository.FindTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("failed to get reporting team"), zap.Error(err)) + } + } + blocks := view.IncidentSummarySectionV3( + incidentEntity, reportingTeamEntity, teamEntity, severityEntity, incidentStatusEntity, + ) color := util.GetColorBySeverity(incidentEntity.SeverityId) att := slack.Attachment{Blocks: blocks, Color: color} _, timestamp, err := isp.client.PostMessage(blazeGroupChannelID, slack.MsgOptionAttachments(att)) @@ -318,3 +370,40 @@ func buildCreateIncidentRequestV2(callback slack.InteractionCallback) (*request. return &createIncidentRequest, nil } + +func buildCreateIncidentRequestV3(callback slack.InteractionCallback) (*request.CreateIncidentRequestV3, error) { + blockActions := callback.View.State.Values + var createIncidentRequest request.CreateIncidentRequestV3 + var requestMap = make(map[string]string) + privateMetadata, err := internal.StringToSlackPrivateMetadata(callback.View.PrivateMetadata) + if err != nil { + return nil, err + } + + for _, actions := range blockActions { + for actionID, a := range actions { + if string(a.Type) == string(slack.METPlainTextInput) { + requestMap[actionID] = a.Value + } + if string(a.Type) == slack.OptTypeStatic { + requestMap[actionID] = a.SelectedOption.Value + } + } + } + + reportingTeamId, err := stringUtil.StringToUint(requestMap["reportingTeamId"]) + responderTeamId, err := stringUtil.StringToUint(requestMap["responderTeamId"]) + severityId, err := stringUtil.StringToUint(requestMap["severityId"]) + if err != nil { + return nil, err + } + createIncidentRequest.Title = requestMap["title"] + createIncidentRequest.Description = requestMap["description"] + createIncidentRequest.SeverityID = severityId + createIncidentRequest.ReportingTeamID = reportingTeamId + createIncidentRequest.ResponderTeamID = responderTeamId + createIncidentRequest.ProductIds = privateMetadata.ProductIds + createIncidentRequest.CreatedBy = callback.User.ID + + return &createIncidentRequest, nil +} diff --git a/internal/processor/action/view/create_incident_modal.go b/internal/processor/action/view/create_incident_modal.go index 22bbab8..385a2a4 100644 --- a/internal/processor/action/view/create_incident_modal.go +++ b/internal/processor/action/view/create_incident_modal.go @@ -1,43 +1,68 @@ package view import ( + "fmt" "houston/common/util" + "houston/internal" "houston/model/severity" "houston/model/team" + response "houston/service/response" "strconv" + "strings" "github.com/slack-go/slack" ) const IncidentTitleLength = 100 -func GenerateModalRequest(teams []team.TeamEntity, severities []severity.SeverityEntity, channel string) slack.ModalViewRequest { - // Create a ModalViewRequest with a header and two inputs +func GenerateModalRequest( + reportingAndResponderTeams *response.ReportingAndResponderTeams, + severities []severity.SeverityEntity, + privateMetadata *internal.SlackPrivateMetadata, +) slack.ModalViewRequest { + defaultReportingTeam := reportingAndResponderTeams.ReportingTeam.DefaultTeam + defaultResponderTeam := reportingAndResponderTeams.ResponderTeam.DefaultTeam + reportingTeams := reportingAndResponderTeams.ReportingTeam.Teams + responderTeams := reportingAndResponderTeams.ResponderTeam.Teams + + productNames := strings.Join(privateMetadata.GetProductNames(), ", ") titleText := slack.NewTextBlockObject(slack.PlainTextType, "Houston", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Send", false, false) - headerText := slack.NewTextBlockObject("mrkdwn", "Start incident", false, false) + headerText := slack.NewTextBlockObject( + "mrkdwn", fmt.Sprintf("Start incident for *%s*", productNames), false, false, + ) headerSection := slack.NewSectionBlock(headerText, nil, nil) - incidentTypeOptions := createOptionBlockObjectsForTeams(teams) - incidentTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Incident type", false, false) - incidentTypePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "The incident type for the incident to create", false, false) - incidentTypeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, incidentTypePlaceholder, "type", incidentTypeOptions...) - incidentTypeBlock := slack.NewInputBlock("incident_type", incidentTypeText, nil, incidentTypeOption) + reportingTeamsOptions := createOptionBlockObjectsForTeamsV2(reportingTeams) + reportingTeamText := slack.NewTextBlockObject(slack.PlainTextType, "Reporting Team", false, false) + reportingTeamPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select reporting team", false, false) + reportingTeamOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, reportingTeamPlaceholder, "reportingTeamId", reportingTeamsOptions...) + if defaultReportingTeam != nil { + defaultReportingTeamOptionText := slack.NewTextBlockObject(slack.PlainTextType, defaultReportingTeam.Name, false, false) + defaultReportingTeamOption := slack.NewOptionBlockObject(fmt.Sprintf("%d", defaultReportingTeam.ID), defaultReportingTeamOptionText, nil) + reportingTeamOption.InitialOption = defaultReportingTeamOption + } + reportingTeamBlock := slack.NewInputBlock("reporting_team", reportingTeamText, nil, reportingTeamOption) + + responderTeamsOptions := createOptionBlockObjectsForTeamsV2(responderTeams) + responderTeamText := slack.NewTextBlockObject(slack.PlainTextType, "Responder Team", false, false) + responderTeamPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select responder team", false, false) + responderTeamOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, responderTeamPlaceholder, "responderTeamId", responderTeamsOptions...) + if defaultResponderTeam != nil { + defaultResponderOptionText := slack.NewTextBlockObject(slack.PlainTextType, defaultResponderTeam.Name, false, false) + defaultResponderOption := slack.NewOptionBlockObject(fmt.Sprintf("%d", defaultResponderTeam.ID), defaultResponderOptionText, nil) + responderTeamOption.InitialOption = defaultResponderOption + } + responderTeamBlock := slack.NewInputBlock("incident_type", responderTeamText, nil, responderTeamOption) severityOptions := createOptionBlockObjectsForSeverity(severities) severityText := slack.NewTextBlockObject(slack.PlainTextType, "Severity", false, false) severityTextPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "The severity for the incident to create", false, false) - severityOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, severityTextPlaceholder, "severity", severityOptions...) + severityOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, severityTextPlaceholder, "severityId", severityOptions...) severityBlock := slack.NewInputBlock("incident_severity", severityText, nil, severityOption) - //pagerDutyImpactedServiceText := slack.NewTextBlockObject(slack.PlainTextType, "Pagerduty", false, false) - //pagerDutyImpactedServicePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select pagerduty impacted services", false, false) - //pagerDutyImpactedServiceElement := slack.NewPlainTextInputBlockElement(pagerDutyImpactedServicePlaceholder, "pagerduty_impacted") - //pagerDutyImpactedService := slack.NewInputBlock("Pagerduty impacted service", pagerDutyImpactedServiceText, nil, pagerDutyImpactedServiceElement) - //pagerDutyImpactedService.Optional = true - incidentTitleText := slack.NewTextBlockObject(slack.PlainTextType, "Incident title", false, false) incidentTitlePlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Write something", false, false) incidentTitleElement := slack.NewPlainTextInputBlockElement(incidentTitlePlaceholder, "title") @@ -54,9 +79,9 @@ func GenerateModalRequest(teams []team.TeamEntity, severities []severity.Severit blocks := slack.Blocks{ BlockSet: []slack.Block{ headerSection, - incidentTypeBlock, + reportingTeamBlock, + responderTeamBlock, severityBlock, - //pagerDutyImpactedService, incidentTitle, incidentDescription, }, @@ -68,7 +93,7 @@ func GenerateModalRequest(teams []team.TeamEntity, severities []severity.Severit Close: closeText, Submit: submitText, Blocks: blocks, - PrivateMetadata: channel, + PrivateMetadata: privateMetadata.String(), CallbackID: string(util.StartIncidentSubmit), } } @@ -92,3 +117,13 @@ func createOptionBlockObjectsForTeams(options []team.TeamEntity) []*slack.Option } return optionBlockObjects } + +func createOptionBlockObjectsForTeamsV2(options []team.TeamDTO) []*slack.OptionBlockObject { + optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) + for _, o := range options { + localObj := o + optionText := slack.NewTextBlockObject(slack.PlainTextType, localObj.Name, false, false) + optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.Itoa(int(localObj.ID)), optionText, nil)) + } + return optionBlockObjects +} diff --git a/internal/processor/action/view/incident_section.go b/internal/processor/action/view/incident_section.go index be6a2b0..ed8b16f 100644 --- a/internal/processor/action/view/incident_section.go +++ b/internal/processor/action/view/incident_section.go @@ -11,7 +11,7 @@ func NewIncidentBlock() map[string]interface{} { "blocks": []slack.Block{ slack.NewActionBlock("start_incident_button", slack.NewButtonBlockElement( - string(util.StartIncident), + util.SelectProduct, "start_incident_button_value", &slack.TextBlockObject{ Type: slack.PlainTextType, @@ -84,9 +84,16 @@ func incidentSectionBlock() *slack.SectionBlock { { Text: &slack.TextBlockObject{ Type: "plain_text", - Text: "Set incident type", + Text: "Set product", }, - Value: util.SetIncidentType, + Value: util.SetProduct, + }, + { + Text: &slack.TextBlockObject{ + Type: "plain_text", + Text: "Set responder team", + }, + Value: util.SetResponderTeam, }, { Text: &slack.TextBlockObject{ diff --git a/internal/processor/action/view/incident_summary_section.go b/internal/processor/action/view/incident_summary_section.go index 340786e..64935a8 100644 --- a/internal/processor/action/view/incident_summary_section.go +++ b/internal/processor/action/view/incident_summary_section.go @@ -2,12 +2,12 @@ package view import ( "fmt" + "github.com/slack-go/slack" "houston/common/util" "houston/model/incident" "houston/model/severity" "houston/model/team" - - "github.com/slack-go/slack" + "strings" ) func IncidentSummarySection(incident *incident.IncidentEntity, team *team.TeamEntity, severity *severity.SeverityEntity, incidentStatus *incident.IncidentStatusEntity) slack.Blocks { @@ -23,6 +23,30 @@ func IncidentSummarySection(incident *incident.IncidentEntity, team *team.TeamEn } } +func IncidentSummarySectionV3( + incident *incident.IncidentEntity, + reportingTeam *team.TeamEntity, + responderTeam *team.TeamEntity, + severity *severity.SeverityEntity, + incidentStatus *incident.IncidentStatusEntity, +) slack.Blocks { + isV2 := reportingTeam == nil + if isV2 { + return IncidentSummarySection(incident, responderTeam, severity, incidentStatus) + } + return slack.Blocks{ + BlockSet: []slack.Block{ + buildSummaryHeader(incident.Title), + buildDescriptionBlock(incident.Description), + buildProductAndReportingTeamSectionBlock(incident, reportingTeam.Name), + buildResponderTeamAndChannelSectionBlock(incident, responderTeam.Name), + buildSeverityAndTicketSectionBlock(severity.Name), + buildStatusAndConferenceLinkSectionBlock(incidentStatus.Name, incident.ConferenceLink), + buildCreatedByAndCreatedAtSectionBlock(incident), + }, + } +} + func IncidentEphemeralMessage(incident *incident.IncidentEntity, blocks slack.Blocks) slack.MsgOption { color := util.GetColorBySeverity(incident.SeverityId) return slack.MsgOptionAttachments(slack.Attachment{Blocks: blocks, Color: color}) @@ -52,6 +76,35 @@ func buildTypeAndChannelSectionBlock(incident *incident.IncidentEntity, teamName return block } +func buildProductAndReportingTeamSectionBlock(incident *incident.IncidentEntity, reportingTeamName string) *slack.SectionBlock { + var productNames []string + for _, product := range incident.Products { + productNames = append(productNames, product.Name) + } + fields := []*slack.TextBlockObject{ + slack.NewTextBlockObject( + "mrkdwn", + fmt.Sprintf("*Product*\n%s", strings.Join(productNames, ", ")), + false, + false, + ), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Reporting team*\n%s", reportingTeamName), false, false), + } + block := slack.NewSectionBlock(nil, fields, nil) + + return block +} + +func buildResponderTeamAndChannelSectionBlock(incident *incident.IncidentEntity, responderTeamName string) *slack.SectionBlock { + fields := []*slack.TextBlockObject{ + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Responder team*\n%s", responderTeamName), false, false), + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Channel*\n<#%s>", incident.SlackChannel), false, false), + } + block := slack.NewSectionBlock(nil, fields, nil) + + return block +} + func buildSeverityAndTicketSectionBlock(severityName string) *slack.SectionBlock { fields := []*slack.TextBlockObject{ slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Severity*\n%s", severityName), false, false), diff --git a/internal/processor/action/view/incident_update_product.go b/internal/processor/action/view/incident_update_product.go new file mode 100644 index 0000000..8ce7455 --- /dev/null +++ b/internal/processor/action/view/incident_update_product.go @@ -0,0 +1,69 @@ +package view + +import ( + "fmt" + "houston/common/util" + "houston/logger" + "houston/model/product" + response "houston/service/response" + "strconv" + + "github.com/slack-go/slack" +) + +func BuildIncidentUpdateProductSelectionModal(channelID string, products *response.ProductsForUpdate) slack.ModalViewRequest { + titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set product of incident", false, false) + closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) + submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) + + incidentProductBlockOption := createIncidentProductBlock(products.Products) + + incidentProductText := slack.NewTextBlockObject(slack.PlainTextType, "Product", false, false) + incidentProductOption := slack.NewOptionsMultiSelectBlockElement( + slack.MultiOptTypeStatic, + nil, + "productIds", + incidentProductBlockOption..., + ) + if products.DefaultProducts != nil { + var initialOptions []*slack.OptionBlockObject + for _, p := range products.DefaultProducts { + optionText := slack.NewTextBlockObject(slack.PlainTextType, p.ProductName, false, false) + initialOptions = append(initialOptions, slack.NewOptionBlockObject(strconv.FormatUint(uint64(p.ProductID), 10), optionText, nil)) + } + incidentProductOption.InitialOptions = initialOptions + } + incidentProductBlock := slack.NewInputBlock("incident_product_modal_request_input", incidentProductText, nil, incidentProductOption) + + blocks := slack.Blocks{ + BlockSet: []slack.Block{ + incidentProductBlock, + }, + } + + logger.Info("[BuildIncidentUpdateProductSelectionModal] channel ID: " + channelID) + + return slack.ModalViewRequest{ + Type: slack.VTModal, + Title: titleText, + Close: closeText, + Submit: submitText, + Blocks: blocks, + PrivateMetadata: channelID, + CallbackID: util.IncidentUpdateProductSelectionSubmit, + } + +} + +func createIncidentProductBlock(options []product.ProductDTO) []*slack.OptionBlockObject { + optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) + for _, o := range options { + txt := fmt.Sprintf("%s", o.ProductName) + optionText := slack.NewTextBlockObject(slack.PlainTextType, txt, false, false) + optionBlockObjects = append( + optionBlockObjects, + slack.NewOptionBlockObject(strconv.FormatUint(uint64(o.ProductID), 10), optionText, nil), + ) + } + return optionBlockObjects +} diff --git a/internal/processor/action/view/incident_update_type.go b/internal/processor/action/view/incident_update_responder_team.go similarity index 54% rename from internal/processor/action/view/incident_update_type.go rename to internal/processor/action/view/incident_update_responder_team.go index cce615d..fbba9af 100644 --- a/internal/processor/action/view/incident_update_type.go +++ b/internal/processor/action/view/incident_update_responder_team.go @@ -4,28 +4,30 @@ import ( "fmt" "houston/common/util" "houston/model/team" + service "houston/service/response" "strconv" "github.com/slack-go/slack" ) -func BuildIncidentUpdateTypeModal(channelID string, teams []team.TeamEntity) slack.ModalViewRequest { +func BuildIncidentUpdateResponderTeamModal(channelID string, responderTeams *service.ProductAndUserTeams) slack.ModalViewRequest { titleText := slack.NewTextBlockObject(slack.PlainTextType, "Set type of incident", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) - headerText := slack.NewTextBlockObject("mrkdwn", "Type", false, false) - headerSection := slack.NewSectionBlock(headerText, nil, nil) + incidentStatusBlockOption := createIncidentTypeBlock(responderTeams.Teams) - incidentStatusBlockOption := createIncidentTypeBlock(teams) - - incidentTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Incident type", false, false) - incidentTypeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "incident_type_modal_request", incidentStatusBlockOption...) - incidentTypeBlock := slack.NewInputBlock("incident_type_modal_request_input", incidentTypeText, nil, incidentTypeOption) + incidentTypeText := slack.NewTextBlockObject(slack.PlainTextType, "Responder team", false, false) + responderTeamOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "incident_responder_team_modal_request", incidentStatusBlockOption...) + if responderTeams.DefaultTeam != nil { + defaultOptionText := slack.NewTextBlockObject(slack.PlainTextType, responderTeams.DefaultTeam.Name, false, false) + defaultOption := slack.NewOptionBlockObject(fmt.Sprintf("%d", responderTeams.DefaultTeam.ID), defaultOptionText, nil) + responderTeamOption.InitialOption = defaultOption + } + incidentTypeBlock := slack.NewInputBlock("incident_responder_team_modal_request_input", incidentTypeText, nil, responderTeamOption) blocks := slack.Blocks{ BlockSet: []slack.Block{ - headerSection, incidentTypeBlock, }, } @@ -42,7 +44,7 @@ func BuildIncidentUpdateTypeModal(channelID string, teams []team.TeamEntity) sla } -func createIncidentTypeBlock(options []team.TeamEntity) []*slack.OptionBlockObject { +func createIncidentTypeBlock(options []team.TeamDTO) []*slack.OptionBlockObject { optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) for _, o := range options { txt := fmt.Sprintf("%s", o.Name) diff --git a/internal/processor/action/view/select_product_modal.go b/internal/processor/action/view/select_product_modal.go new file mode 100644 index 0000000..e315070 --- /dev/null +++ b/internal/processor/action/view/select_product_modal.go @@ -0,0 +1,62 @@ +package view + +import ( + "fmt" + "houston/common/util" + "houston/internal" + "houston/model/product" + service "houston/service/response" + "strconv" + + "github.com/slack-go/slack" +) + +func SelectProductModal( + productsOfUser *service.ProductsOfUser, + slackPrivateMetadata *internal.SlackPrivateMetadata, +) slack.ModalViewRequest { + titleText := slack.NewTextBlockObject(slack.PlainTextType, "Houston", false, false) + closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) + submitText := slack.NewTextBlockObject(slack.PlainTextType, "Send", false, false) + + headerText := slack.NewTextBlockObject("mrkdwn", "Select product", false, false) + headerSection := slack.NewSectionBlock(headerText, nil, nil) + + productOptions := createOptionBlockObjectsForProducts(productsOfUser.Products) + productText := slack.NewTextBlockObject(slack.PlainTextType, "Product", false, false) + productPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Select product", false, false) + productOption := slack.NewOptionsMultiSelectBlockElement(slack.MultiOptTypeStatic, productPlaceholder, "productIds", productOptions...) + if productsOfUser.DefaultProduct != nil { + defaultProductOptionText := slack.NewTextBlockObject(slack.PlainTextType, productsOfUser.DefaultProduct.ProductName, false, false) + defaultProductOption := slack.NewOptionBlockObject(fmt.Sprintf("%d", productsOfUser.DefaultProduct.ProductID), defaultProductOptionText, nil) + productOption.InitialOptions = []*slack.OptionBlockObject{defaultProductOption} + } + productBlock := slack.NewInputBlock("product_block", productText, nil, productOption) + + blocks := slack.Blocks{ + BlockSet: []slack.Block{ + headerSection, + productBlock, + }, + } + + return slack.ModalViewRequest{ + Type: slack.VTModal, + Title: titleText, + Close: closeText, + Submit: submitText, + Blocks: blocks, + PrivateMetadata: slackPrivateMetadata.String(), + CallbackID: util.SelectProductSubmit, + } +} + +func createOptionBlockObjectsForProducts(options []product.ProductDTO) []*slack.OptionBlockObject { + optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) + for _, o := range options { + localObj := o + optionText := slack.NewTextBlockObject(slack.PlainTextType, localObj.ProductName, false, false) + optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(strconv.Itoa(int(localObj.ProductID)), optionText, nil)) + } + return optionBlockObjects +} diff --git a/internal/processor/event_type_interactive_processor.go b/internal/processor/event_type_interactive_processor.go index 6c995d8..9e3dff3 100644 --- a/internal/processor/event_type_interactive_processor.go +++ b/internal/processor/event_type_interactive_processor.go @@ -2,7 +2,6 @@ package processor import ( "fmt" - "github.com/spf13/viper" "gorm.io/gorm" "houston/appcontext" "houston/common/util" @@ -14,6 +13,9 @@ import ( "houston/model/team" "houston/pkg/slackbot" incidentService "houston/service/incident/impl" + "houston/service/orchestration" + "houston/service/products" + "houston/service/productsTeams" rcaService "houston/service/rca/impl" slack2 "houston/service/slack" @@ -28,6 +30,7 @@ type interactiveEventProcessor interface { type BlockActionProcessor struct { socketModeClient *socketmode.Client + selectProductBlockAction *action.SelectProductBlockAction startIncidentBlockAction *action.StartIncidentBlockAction showIncidentsAction *action.ShowIncidentsAction helpCommandsAction *action.HelpCommandsAction @@ -35,6 +38,7 @@ type BlockActionProcessor struct { incidentResolveAction *action.ResolveIncidentAction incidentUpdateAction *action.UpdateIncidentAction incidentUpdateTypeAction *action.IncidentUpdateTypeAction + incidentUpdateProductAction *action.IncidentUpdateProductAction incidentUpdateSeverityAction *action.IncidentUpdateSevertityAction incidentUpdateTitleAction *action.IncidentUpdateTitleAction incidentUpdateDescriptionAction *action.IncidentUpdateDescriptionAction @@ -55,12 +59,17 @@ func NewBlockActionProcessor( incidentServiceV2 *incidentService.IncidentServiceV2, slackService *slack2.SlackService, rcaService *rcaService.RcaService, + productsService products.ProductService, + productTeamService productsTeams.ProductTeamsService, + orchestrator orchestration.IncidentOrchestrator, ) *BlockActionProcessor { return &BlockActionProcessor{ - socketModeClient: socketModeClient, - startIncidentBlockAction: action.NewStartIncidentBlockAction(socketModeClient, teamService, - severityService), + socketModeClient: socketModeClient, + selectProductBlockAction: action.NewSelectProductBlockAction(socketModeClient, productsService, orchestrator), + startIncidentBlockAction: action.NewStartIncidentBlockAction( + socketModeClient, teamService, severityService, orchestrator, + ), showIncidentsAction: action.ShowIncidentsBlockAction(socketModeClient, teamService), helpCommandsAction: action.NewHelpCommandsAction(socketModeClient), assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository), @@ -69,7 +78,17 @@ func NewBlockActionProcessor( incidentUpdateAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository, tagService, teamService, severityService), incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository, - teamService, severityService, slackbotClient, incidentServiceV2), + teamService, severityService, slackbotClient, incidentServiceV2, orchestrator), + incidentUpdateProductAction: action.NewIncidentUpdateProductAction( + socketModeClient, + incidentRepository, + productsService, + productTeamService, + severityService, + slackbotClient, + incidentServiceV2, + orchestrator, + ), incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository, severityService, teamService, slackbotClient, incidentServiceV2), incidentUpdateTitleAction: action.NewIncidentUpdateTitleAction(socketModeClient, incidentRepository, @@ -103,9 +122,9 @@ func (bap *BlockActionProcessor) ProcessCommand(callback slack.InteractionCallba zap.String("user_name", callback.User.Name)) switch actionId { - case util.StartIncident: + case util.SelectProduct: { - bap.startIncidentBlockAction.ProcessAction(request, callback) + bap.selectProductBlockAction.ProcessAction(request, callback) } case util.ShowIncidents: { @@ -160,7 +179,11 @@ func (bap *BlockActionProcessor) processIncidentCommands(callback slack.Interact { bap.incidentUpdateAction.IncidentUpdateStatusRequestProcess(callback, request) } - case util.SetIncidentType: + case util.SetProduct: + { + bap.incidentUpdateProductAction.IncidentSetProductRequestProcess(callback, request) + } + case util.SetResponderTeam: { bap.incidentUpdateTypeAction.IncidentUpdateTypeRequestProcess(callback, request) } @@ -201,11 +224,13 @@ type ViewSubmissionProcessor struct { socketModeClient *socketmode.Client incidentChannelMessageUpdateAction *action.IncidentChannelMessageUpdateAction createIncidentAction *action.CreateIncidentAction + selectProductAction *action.StartIncidentBlockAction assignIncidentAction *action.AssignIncidentAction updateIncidentAction *action.UpdateIncidentAction incidentUpdateTitleAction *action.IncidentUpdateTitleAction incidentUpdateDescriptionAction *action.IncidentUpdateDescriptionAction incidentUpdateSeverityAction *action.IncidentUpdateSevertityAction + incidentUpdateProductAction *action.IncidentUpdateProductAction incidentUpdateTypeAction *action.IncidentUpdateTypeAction incidentAdditionalAction *action.IncidentRCASectionAction showIncidentSubmitAction *action.ShowIncidentsSubmitAction @@ -217,6 +242,8 @@ type ViewSubmissionProcessor struct { func NewViewSubmissionProcessor( socketModeClient *socketmode.Client, incidentRepository *incident.Repository, + productService products.ProductService, + productTeamService productsTeams.ProductTeamsService, teamService *team.Repository, severityService *severity.Repository, tagService *tag.Repository, @@ -225,6 +252,7 @@ func NewViewSubmissionProcessor( db *gorm.DB, rcaService *rcaService.RcaService, incidentServiceV2 *incidentService.IncidentServiceV2, + orchestrator orchestration.IncidentOrchestrator, ) *ViewSubmissionProcessor { slackService := slack2.NewSlackService() return &ViewSubmissionProcessor{ @@ -233,6 +261,7 @@ func NewViewSubmissionProcessor( incidentRepository, teamService, severityService), createIncidentAction: action.NewCreateIncidentProcessor(socketModeClient, incidentRepository, teamService, severityService, slackbotClient, db), + selectProductAction: action.NewStartIncidentBlockAction(socketModeClient, teamService, severityService, orchestrator), assignIncidentAction: action.NewAssignIncidentAction(socketModeClient, incidentRepository), updateIncidentAction: action.NewIncidentUpdateAction(socketModeClient, incidentRepository, tagService, teamService, severityService), @@ -241,8 +270,18 @@ func NewViewSubmissionProcessor( incidentUpdateDescriptionAction: action.NewIncidentUpdateDescriptionAction(socketModeClient, incidentRepository), incidentUpdateSeverityAction: action.NewIncidentUpdateSeverityAction(socketModeClient, incidentRepository, severityService, teamService, slackbotClient, incidentServiceV2), + incidentUpdateProductAction: action.NewIncidentUpdateProductAction( + socketModeClient, + incidentRepository, + productService, + productTeamService, + severityService, + slackbotClient, + incidentServiceV2, + orchestrator, + ), incidentUpdateTypeAction: action.NewIncidentUpdateTypeAction(socketModeClient, incidentRepository, - teamService, severityService, slackbotClient, incidentServiceV2), + teamService, severityService, slackbotClient, incidentServiceV2, orchestrator), showIncidentSubmitAction: action.NewShowIncidentsSubmitAction(socketModeClient, incidentRepository, teamRepository), incidentDuplicateAction: action.NewDuplicateIncidentProcessor(socketModeClient, incidentRepository, @@ -268,13 +307,11 @@ func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCal switch callbackId { case util.StartIncidentSubmit: { - createIncidentV2Enabled := viper.GetBool("CREATE_INCIDENT_V2_ENABLED") - if createIncidentV2Enabled { - vsp.createIncidentAction.CreateIncidentModalCommandProcessingV2(callback, request) - } else { - vsp.createIncidentAction.CreateIncidentModalCommandProcessing(callback, request) - } + vsp.createIncidentAction.CreateIncidentModalCommandProcessingV3(callback, request) } + case util.SelectProductSubmit: + vsp.selectProductAction.ProcessAction(request, callback) + case util.AssignIncidentRoleSubmit: { vsp.assignIncidentAction.IncidentAssignModalCommandProcessing(callback, request) @@ -299,6 +336,14 @@ func (vsp *ViewSubmissionProcessor) ProcessCommand(callback slack.InteractionCal { vsp.incidentUpdateSeverityAction.IncidentUpdateSeverityJustification(callback, request, callback.User) } + case util.IncidentUpdateProductSelectionSubmit: + { + vsp.incidentUpdateProductAction.IncidentUpdateProductSelectionRequestProcess(callback, request) + } + case util.SetIncidentProductSubmit: + { + vsp.incidentUpdateProductAction.IncidentUpdateProductRequestProcess(callback, request, callback.Channel, callback.User) + } case util.SetIncidentTypeSubmit: { vsp.incidentUpdateTypeAction.IncidentUpdateType(callback, request, callback.Channel, callback.User) diff --git a/internal/processor/open_set_team_view_modal_command_processor.go b/internal/processor/open_set_team_view_modal_command_processor.go index 4d8d719..5ceda87 100644 --- a/internal/processor/open_set_team_view_modal_command_processor.go +++ b/internal/processor/open_set_team_view_modal_command_processor.go @@ -6,6 +6,7 @@ import ( "houston/internal/processor/action" "houston/logger" "houston/pkg/slackbot" + "houston/service/orchestration" ) type OpenSetTeamViewModalCommandProcessor struct { @@ -18,10 +19,11 @@ const openSetTeamViewModalCommandProcessorLogTag = "[open_set_team_view_modal_co func NewOpenSetTeamViewModalCommandProcessor( socketModeClient *socketmode.Client, slackBot *slackbot.Client, + incidentOrchestrator orchestration.IncidentOrchestrator, ) *OpenSetTeamViewModalCommandProcessor { return &OpenSetTeamViewModalCommandProcessor{ socketModeClient: socketModeClient, - openSetTeamViewModalCommandAction: action.NewOpenSetTeamViewModalCommandAction(socketModeClient, slackBot), + openSetTeamViewModalCommandAction: action.NewOpenSetTeamViewModalCommandAction(socketModeClient, slackBot, incidentOrchestrator), } } diff --git a/internal/processor/start_incident_command_processor.go b/internal/processor/start_incident_command_processor.go index 07acebf..d01e365 100644 --- a/internal/processor/start_incident_command_processor.go +++ b/internal/processor/start_incident_command_processor.go @@ -6,9 +6,13 @@ import ( "houston/internal/processor/action" "houston/logger" "houston/pkg/slackbot" + "houston/service/orchestration" + "houston/service/products" ) type StartIncidentCommandProcessor struct { + productsService products.ProductService + incidentOrchestrator orchestration.IncidentOrchestrator socketModeClient *socketmode.Client startIncidentCommandAction *action.StartIncidentCommandAction } @@ -16,12 +20,15 @@ type StartIncidentCommandProcessor struct { const StartIncidentCommandProcessorLogTag = "[start_incident_command_processor]" func NewStartIncidentCommandProcessor( + productsService products.ProductService, + incidentOrchestrator orchestration.IncidentOrchestrator, socketModeClient *socketmode.Client, slackBot *slackbot.Client, ) *StartIncidentCommandProcessor { return &StartIncidentCommandProcessor{ + productsService: productsService, socketModeClient: socketModeClient, - startIncidentCommandAction: action.NewStartIncidentCommandAction(socketModeClient, slackBot), + startIncidentCommandAction: action.NewStartIncidentCommandAction(productsService, incidentOrchestrator, socketModeClient, slackBot), } } diff --git a/internal/resolver/houston_command_resolver.go b/internal/resolver/houston_command_resolver.go index b5587f9..4220328 100644 --- a/internal/resolver/houston_command_resolver.go +++ b/internal/resolver/houston_command_resolver.go @@ -9,25 +9,33 @@ import ( "houston/internal/processor" "houston/logger" "houston/pkg/slackbot" + "houston/service/orchestration" + "houston/service/products" rcaService "houston/service/rca/impl" "strings" ) type HoustonCommandResolver struct { - socketModeClient *socketmode.Client - slackBotClient *slackbot.Client - rcaService *rcaService.RcaService + socketModeClient *socketmode.Client + slackBotClient *slackbot.Client + rcaService *rcaService.RcaService + productsService products.ProductService + incidentOrchestrator orchestration.IncidentOrchestrator } func NewHoustonCommandResolver( socketModeClient *socketmode.Client, slackBotClient *slackbot.Client, rcaService *rcaService.RcaService, + productsService products.ProductService, + incidentOrchestrator orchestration.IncidentOrchestrator, ) *HoustonCommandResolver { return &HoustonCommandResolver{ - socketModeClient: socketModeClient, - slackBotClient: slackBotClient, - rcaService: rcaService, + socketModeClient: socketModeClient, + slackBotClient: slackBotClient, + rcaService: rcaService, + productsService: productsService, + incidentOrchestrator: incidentOrchestrator, } } @@ -48,6 +56,8 @@ func (resolver *HoustonCommandResolver) Resolve(evt *socketmode.Event) processor switch { case strings.HasPrefix(params, string(internal.StartIncidentParam)): commandProcessor = processor.NewStartIncidentCommandProcessor( + resolver.productsService, + resolver.incidentOrchestrator, resolver.socketModeClient, resolver.slackBotClient, ) @@ -68,6 +78,7 @@ func (resolver *HoustonCommandResolver) Resolve(evt *socketmode.Event) processor commandProcessor = processor.NewOpenSetTeamViewModalCommandProcessor( resolver.socketModeClient, resolver.slackBotClient, + resolver.incidentOrchestrator, ) case strings.HasPrefix(params, internal.SetTeamParam): diff --git a/internal/slack_private_metadata.go b/internal/slack_private_metadata.go new file mode 100644 index 0000000..3a30e83 --- /dev/null +++ b/internal/slack_private_metadata.go @@ -0,0 +1,74 @@ +package internal + +import ( + "encoding/json" + "fmt" +) + +type SlackPrivateMetadata struct { + Channel string `json:"channel"` + ProductIds []uint `json:"productIds"` + ProductNames []string `json:"productNames"` + TriggerId string `json:"triggerId"` +} + +func NewSlackPrivateMetadata() *SlackPrivateMetadata { + return &SlackPrivateMetadata{} +} + +func StringToSlackPrivateMetadata(jsonString string) (*SlackPrivateMetadata, error) { + var slackPrivateMetadata SlackPrivateMetadata + + // Unmarshal the JSON string into the struct + err := json.Unmarshal([]byte(jsonString), &slackPrivateMetadata) + if err != nil { + fmt.Println("Error:", err) + return nil, err + } + return &slackPrivateMetadata, nil +} + +func (s *SlackPrivateMetadata) String() string { + jsonData, err := json.Marshal(s) + if err != nil { + fmt.Println("Error:", err) + return "" + } + return string(jsonData) +} + +func (s *SlackPrivateMetadata) SetChannel(channel string) *SlackPrivateMetadata { + s.Channel = channel + return s +} + +func (s *SlackPrivateMetadata) GetChannel() string { + return s.Channel +} + +func (s *SlackPrivateMetadata) SetProductIds(productIds []uint) *SlackPrivateMetadata { + s.ProductIds = productIds + return s +} + +func (s *SlackPrivateMetadata) GetProductIds() []uint { + return s.ProductIds +} + +func (s *SlackPrivateMetadata) SetProductNames(productNames []string) *SlackPrivateMetadata { + s.ProductNames = productNames + return s +} + +func (s *SlackPrivateMetadata) GetProductNames() []string { + return s.ProductNames +} + +func (s *SlackPrivateMetadata) SetTriggerId(triggerId string) *SlackPrivateMetadata { + s.TriggerId = triggerId + return s +} + +func (s *SlackPrivateMetadata) GetTriggerId() string { + return s.TriggerId +} diff --git a/model/incident/entity.go b/model/incident/entity.go index dc44b09..9eb3bfa 100644 --- a/model/incident/entity.go +++ b/model/incident/entity.go @@ -45,28 +45,29 @@ const ( // IncidentEntity all the incident created will go in this table type IncidentEntity struct { gorm.Model - Title string `gorm:"column:title"` - Description string `gorm:"column:description"` - Status uint `gorm:"column:status"` - SeverityId uint `gorm:"column:severity_id"` - IncidentName string `gorm:"column:incident_name"` - SlackChannel string `gorm:"column:slack_channel"` - DetectionTime *time.Time `gorm:"column:detection_time"` - StartTime time.Time `gorm:"column:start_time"` - EndTime *time.Time `gorm:"column:end_time"` - TeamId uint `gorm:"column:team_id"` - JiraLinks pq.StringArray `gorm:"column:jira_links;type:text[]"` - ConfluenceId *string `gorm:"column:confluence_id"` - SeverityTat time.Time `gorm:"column:severity_tat"` - RemindMeAt *time.Time `gorm:"column:remind_me_at"` - EnableReminder bool `gorm:"column:enable_reminder"` - CreatedBy string `gorm:"column:created_by"` - UpdatedBy string `gorm:"column:updated_by"` - MetaData JSON `gorm:"column:meta_data;type:jsonb"` - RCA string `gorm:"column:rca_text"` - ConferenceId string `gorm:"column:conference_id"` - ConferenceLink string `gorm:"column:conference_link"` - Products []product.ProductEntity `gorm:"many2many:incident_products;"` + Title string `gorm:"column:title"` + Description string `gorm:"column:description"` + Status uint `gorm:"column:status"` + SeverityId uint `gorm:"column:severity_id"` + IncidentName string `gorm:"column:incident_name"` + SlackChannel string `gorm:"column:slack_channel"` + DetectionTime *time.Time `gorm:"column:detection_time"` + StartTime time.Time `gorm:"column:start_time"` + EndTime *time.Time `gorm:"column:end_time"` + TeamId uint `gorm:"column:team_id"` + JiraLinks pq.StringArray `gorm:"column:jira_links;type:text[]"` + ConfluenceId *string `gorm:"column:confluence_id"` + SeverityTat time.Time `gorm:"column:severity_tat"` + RemindMeAt *time.Time `gorm:"column:remind_me_at"` + EnableReminder bool `gorm:"column:enable_reminder"` + CreatedBy string `gorm:"column:created_by"` + UpdatedBy string `gorm:"column:updated_by"` + MetaData JSON `gorm:"column:meta_data;type:jsonb"` + RCA string `gorm:"column:rca_text"` + ConferenceId string `gorm:"column:conference_id"` + ConferenceLink string `gorm:"column:conference_link"` + ReportingTeamId *uint `gorm:"column:reporting_team_id"` + Products []product.ProductEntity `gorm:"many2many:incident_products;"` } func (IncidentEntity) TableName() string { @@ -79,20 +80,21 @@ func (i *IncidentEntity) ToDTO() IncidentDTO { products = append(products, *p.ToDTO()) } return IncidentDTO{ - ID: i.ID, - Title: i.Title, - Description: i.Description, - Status: i.Status, - SeverityId: i.SeverityId, - IncidentName: i.IncidentName, - SlackChannel: i.SlackChannel, - TeamId: i.TeamId, - JiraLinks: i.JiraLinks, - ConfluenceId: i.ConfluenceId, - CreatedBy: i.CreatedBy, - RCA: i.RCA, - ConferenceId: i.ConferenceId, - Products: products, + ID: i.ID, + Title: i.Title, + Description: i.Description, + Status: i.Status, + SeverityId: i.SeverityId, + IncidentName: i.IncidentName, + SlackChannel: i.SlackChannel, + TeamId: i.TeamId, + JiraLinks: i.JiraLinks, + ConfluenceId: i.ConfluenceId, + CreatedBy: i.CreatedBy, + RCA: i.RCA, + ConferenceId: i.ConferenceId, + ReportingTeamId: i.ReportingTeamId, + Products: products, } } diff --git a/model/incident/incident.go b/model/incident/incident.go index 81a745c..dafa763 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -69,19 +69,20 @@ func (r *Repository) CreateIncidentEntity(request *CreateIncidentDTO, tx *gorm.D } incidentEntity := &IncidentEntity{ - Title: request.Title, - Description: request.Description, - Status: incidentStatusEntity.ID, - SeverityId: uint(severityId), - DetectionTime: request.DetectionTime, - StartTime: request.StartTime, - TeamId: uint(teamId), - EnableReminder: request.EnableReminder, - SeverityTat: time.Now().AddDate(0, 0, severity.Sla), - CreatedBy: request.CreatedBy, - UpdatedBy: request.UpdatedBy, - MetaData: request.MetaData, - Products: products, + Title: request.Title, + Description: request.Description, + Status: incidentStatusEntity.ID, + SeverityId: uint(severityId), + DetectionTime: request.DetectionTime, + StartTime: request.StartTime, + TeamId: uint(teamId), + EnableReminder: request.EnableReminder, + SeverityTat: time.Now().AddDate(0, 0, severity.Sla), + CreatedBy: request.CreatedBy, + UpdatedBy: request.UpdatedBy, + MetaData: request.MetaData, + ReportingTeamId: request.ReportingTeamID, + Products: products, } tx.Create(incidentEntity) @@ -310,7 +311,7 @@ func (r *Repository) GetOpenIncidents(limit int) (*[]IncidentSeverityTeamDTO, er func (r *Repository) FindIncidentByChannelId(channelId string) (*IncidentEntity, error) { var incidentEntity IncidentEntity - result := r.gormClient.Find(&incidentEntity, "slack_channel = ?", channelId) + result := r.gormClient.Preload("Products").Find(&incidentEntity, "slack_channel = ?", channelId) if result.Error != nil { return nil, result.Error } @@ -395,7 +396,7 @@ func (r *Repository) FetchAllIncidentsPaginated( from string, to string, ) ([]IncidentEntity, int, error) { - var query = r.gormClient.Model([]IncidentEntity{}) + var query = r.gormClient.Model([]IncidentEntity{}).Preload("Products") var incidentEntity []IncidentEntity if len(TeamsId) != 0 { query = query.Where("team_id IN ?", TeamsId) diff --git a/model/incident/model.go b/model/incident/model.go index 99da138..13d8f90 100644 --- a/model/incident/model.go +++ b/model/incident/model.go @@ -34,27 +34,27 @@ func (j JSON) Value() (driver.Value, error) { } type CreateIncidentDTO struct { - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Severity string `json:"severity,omitempty"` - Pagerduty string `json:"pagerduty,omitempty"` - Status IncidentStatus `json:"status,omitempty"` - IncidentName string `json:"incident_name,omitempty"` - SlackChannel string `json:"slack_channel,omitempty"` - DetectionTime *time.Time `json:"detection_time,omitempty"` - StartTime time.Time `json:"start_time,omitempty"` - EndTime *time.Time `json:"end_time,omitempty"` - TeamId string `json:"type,omitempty"` - JiraId *string `json:"jira_id,omitempty"` - ConfluenceId *string `json:"confluence_id,omitempty"` - SeverityTat *time.Time `json:"severity_tat,omitempty"` - RemindMeAt *time.Time `json:"remind_me_at,omitempty"` - EnableReminder bool `json:"enable_reminder,omitempty"` - CreatedBy string `json:"created_by,omitempty"` - UpdatedBy string `json:"updated_by,omitempty"` - MetaData JSON `json:"meta_data,omitempty"` - ProductIds []uint `json:"product_ids,omitempty"` - AssignerTeamID uint `json:"assigner_team_id,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Severity string `json:"severity,omitempty"` + Pagerduty string `json:"pagerduty,omitempty"` + Status IncidentStatus `json:"status,omitempty"` + IncidentName string `json:"incident_name,omitempty"` + SlackChannel string `json:"slack_channel,omitempty"` + DetectionTime *time.Time `json:"detection_time,omitempty"` + StartTime time.Time `json:"start_time,omitempty"` + EndTime *time.Time `json:"end_time,omitempty"` + TeamId string `json:"type,omitempty"` + JiraId *string `json:"jira_id,omitempty"` + ConfluenceId *string `json:"confluence_id,omitempty"` + SeverityTat *time.Time `json:"severity_tat,omitempty"` + RemindMeAt *time.Time `json:"remind_me_at,omitempty"` + EnableReminder bool `json:"enable_reminder,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` + MetaData JSON `json:"meta_data,omitempty"` + ProductIds []uint `json:"product_ids,omitempty"` + ReportingTeamID *uint `json:"reporting_team_id,omitempty"` } type IncidentSeverityTeamDTO struct { @@ -92,19 +92,20 @@ type TeamMetricDetailsOfIncident struct { } type IncidentDTO struct { - ID uint `json:"id,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - SeverityId uint `json:"severity_id,omitempty"` - Status uint `json:"status,omitempty"` - IncidentName string `json:"incident_name,omitempty"` - SlackChannel string `json:"slack_channel,omitempty"` - TeamId uint `json:"team_id,omitempty"` - JiraLinks pq.StringArray `json:"jira_links,omitempty"` - ConfluenceId *string `json:"confluence_id,omitempty"` - CreatedBy string `json:"created_by,omitempty"` - RCA string `json:"rca,omitempty"` - ConferenceId string `json:"conference_id,omitempty"` - ConferenceLink string `json:"conference_link,omitempty"` - Products []product.ProductDTO `json:"products,omitempty"` + ID uint `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + SeverityId uint `json:"severity_id,omitempty"` + Status uint `json:"status,omitempty"` + IncidentName string `json:"incident_name,omitempty"` + SlackChannel string `json:"slack_channel,omitempty"` + TeamId uint `json:"team_id,omitempty"` + JiraLinks pq.StringArray `json:"jira_links,omitempty"` + ConfluenceId *string `json:"confluence_id,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + RCA string `json:"rca,omitempty"` + ConferenceId string `json:"conference_id,omitempty"` + ConferenceLink string `json:"conference_link,omitempty"` + ReportingTeamId *uint `json:"reporting_team_id,omitempty"` + Products []product.ProductDTO `json:"products,omitempty"` } diff --git a/model/incident_channel/incident_channel.go b/model/incident_channel/incident_channel.go index ab8b260..a2d98de 100644 --- a/model/incident_channel/incident_channel.go +++ b/model/incident_channel/incident_channel.go @@ -16,7 +16,7 @@ func NewIncidentChannelRepository( } } -func (r *Repository) GetIncidentChannels(incidentId uint) (*[]IncidentChannelEntity, error) { +func (r *Repository) GetIncidentChannels(incidentId uint) ([]IncidentChannelEntity, error) { var incidentChannels []IncidentChannelEntity result := r.gormClient.Find(&incidentChannels, "incident_id = ?", incidentId) @@ -28,5 +28,5 @@ func (r *Repository) GetIncidentChannels(incidentId uint) (*[]IncidentChannelEnt return nil, nil } - return &incidentChannels, nil + return incidentChannels, nil } diff --git a/model/products_teams/products_teams_repository_impl.go b/model/products_teams/products_teams_repository_impl.go index a05a1b0..e094964 100644 --- a/model/products_teams/products_teams_repository_impl.go +++ b/model/products_teams/products_teams_repository_impl.go @@ -94,15 +94,17 @@ func (repo *productsTeamsRepositoryImpl) GetProductTeamsByProductID(productID ui return nil, result.Error } if result.RowsAffected == 0 { - return nil, nil + return nil, gorm.ErrRecordNotFound } teamDTOs := make([]team.TeamDTO, 0) for _, e := range entities { - teamDTOs = append(teamDTOs, team.TeamDTO{ - ID: e.Team.ID, - Name: e.Team.Name, - Active: e.Team.Active, - }) + if e.Team.ID > 0 { + teamDTOs = append(teamDTOs, team.TeamDTO{ + ID: e.Team.ID, + Name: e.Team.Name, + Active: e.Team.Active, + }) + } } dto := ProductTeamsDTO{ Product: product.ProductDTO{ProductID: entities[0].ProductID, ProductName: entities[0].Product.Name}, diff --git a/model/team/entity.go b/model/team/entity.go index c48b7dd..af0796e 100644 --- a/model/team/entity.go +++ b/model/team/entity.go @@ -23,8 +23,8 @@ func (TeamEntity) TableName() string { return "team" } -func (entity TeamEntity) ToDTO() TeamDTO { - return TeamDTO{ +func (entity *TeamEntity) ToDTO() *TeamDTO { + return &TeamDTO{ ID: entity.ID, Name: entity.Name, SlackUserIds: entity.SlackUserIds, diff --git a/model/teamSeverity/entity.go b/model/teamSeverity/entity.go index 05172ed..10e191f 100644 --- a/model/teamSeverity/entity.go +++ b/model/teamSeverity/entity.go @@ -26,7 +26,7 @@ func (entity TeamSeverityEntity) ToDTO() TeamSeverityDTO { TeamID: entity.TeamID, SeverityID: entity.SeverityID, Sla: entity.Sla, - TeamDTO: entity.Team.ToDTO(), + TeamDTO: *entity.Team.ToDTO(), SeverityDTO: entity.Severity.ToDTO(), } } diff --git a/model/teamUser/entity.go b/model/teamUser/entity.go index c05fa04..4d36593 100644 --- a/model/teamUser/entity.go +++ b/model/teamUser/entity.go @@ -24,7 +24,7 @@ func (entity TeamUserEntity) ToDTO() *TeamUserDTO { ID: entity.ID, TeamID: entity.TeamID, UserID: entity.UserID, - Team: entity.Team.ToDTO(), + Team: *entity.Team.ToDTO(), User: *entity.User.ToDTO(), } } diff --git a/repository/teamUser/team_user_repository_impl.go b/repository/teamUser/team_user_repository_impl.go index 6be624f..11b4726 100644 --- a/repository/teamUser/team_user_repository_impl.go +++ b/repository/teamUser/team_user_repository_impl.go @@ -40,7 +40,7 @@ func (repo *teamUserRepositoryImpl) GetTeamUserByTeamIdAndUserId(teamId, userId func (repo *teamUserRepositoryImpl) GetTeamsByUserId(userId uint) ([]teamUser.TeamUserEntity, error) { var teamUsers []teamUser.TeamUserEntity - result := repo.gormClient.Where("user_id = ?", userId).Preload("Team").Find(&teamUsers) + result := repo.gormClient.Preload("Team").Find(&teamUsers, "user_id = ?", userId) if result.Error != nil { return nil, result.Error } diff --git a/service/incident/impl/incident_service_test.go b/service/incident/impl/incident_service_test.go index 01b1d3d..59a2319 100644 --- a/service/incident/impl/incident_service_test.go +++ b/service/incident/impl/incident_service_test.go @@ -24,31 +24,32 @@ import ( type IncidentServiceSuite struct { suite.Suite - slackService mocks.ISlackServiceMock - incidentService *IncidentServiceV2 - incidentRepository mocks.IIncidentRepositoryMock - teamRepository mocks.ITeamRepositoryMock - userRepository mocks.IUserRepositoryMock - teamUserService mocks.ITeamUserServiceMock - severityRepository mocks.ISeverityRepositoryMock - krakatoaService mocks.IKrakatoaServiceMock - incidentChannelService mocks.IIncidentChannelServiceMock - incidentJiraService mocks.IncidentJiraServiceMock - tagService mocks.ITagServiceMock - calendarService mocks.ICalendarServiceMock - rcaService mocks.IRcaServiceMock - mockDirectory string - mockPngName string - mockCsvName string - mockIncidentId uint - mockUserEmail string - previousSeverityId uint - updatedSeverityId uint - previousStatusId uint - updatedStatusId uint - previousTeamId uint - updatedTeamId uint - mockValidJiraLink string + slackService mocks.ISlackServiceMock + incidentService *IncidentServiceV2 + incidentRepository mocks.IIncidentRepositoryMock + teamRepository mocks.ITeamRepositoryMock + userRepository mocks.IUserRepositoryMock + teamUserService mocks.ITeamUserServiceMock + severityRepository mocks.ISeverityRepositoryMock + krakatoaService mocks.IKrakatoaServiceMock + incidentChannelService mocks.IIncidentChannelServiceMock + incidentJiraService mocks.IncidentJiraServiceMock + tagService mocks.ITagServiceMock + calendarService mocks.ICalendarServiceMock + rcaService mocks.IRcaServiceMock + mockDirectory string + mockPngName string + mockCsvName string + mockIncidentId uint + mockIncidentSlackChannelID string + mockUserEmail string + previousSeverityId uint + updatedSeverityId uint + previousStatusId uint + updatedStatusId uint + previousTeamId uint + updatedTeamId uint + mockValidJiraLink string } func (suite *IncidentServiceSuite) Test_UpdateIncident_Success() { @@ -67,6 +68,9 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_Success() { suite.incidentRepository.FindIncidentByIdMock.When(suite.mockIncidentId). Then(mockIncident, nil) + suite.incidentRepository.FindIncidentByChannelIdMock.When(suite.mockIncidentSlackChannelID). + Then(mockIncident, nil) + suite.slackService.GetUserByEmailMock.When(suite.mockUserEmail). Then(mockUser, nil) @@ -123,7 +127,7 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_Success() { Status: "2", SeverityId: "3", TeamId: "2", - MetaData: *mockMetaData, + MetaData: mockMetaData, }, suite.mockUserEmail, ) @@ -317,13 +321,13 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_SlackError() { func GetMockIncident() *incident.IncidentEntity { return &incident.IncidentEntity{ - Model: gorm.Model{}, + Model: gorm.Model{ID: 1}, Title: "Mock Title", Description: "Mock Description", Status: 1, SeverityId: 4, IncidentName: "Mock Name", - SlackChannel: "1234", + SlackChannel: "mock_channel", DetectionTime: nil, StartTime: time.Time{}, EndTime: nil, @@ -391,8 +395,8 @@ func GetMockStatusWithId(statusId uint) *incident.IncidentStatusEntity { } } -func GetMockChannels() *[]incident_channel.IncidentChannelEntity { - return &[]incident_channel.IncidentChannelEntity{ +func GetMockChannels() []incident_channel.IncidentChannelEntity { + return []incident_channel.IncidentChannelEntity{ { Model: gorm.Model{ ID: 1, @@ -448,6 +452,7 @@ func (suite *IncidentServiceSuite) SetupTest() { suite.calendarService = *mocks.NewICalendarServiceMock(suite.T()) suite.rcaService = *mocks.NewIRcaServiceMock(suite.T()) suite.mockIncidentId = 1 + suite.mockIncidentSlackChannelID = "mock_channel" suite.mockUserEmail = "testemail@testemail.com" suite.mockValidJiraLink = "mock_base_url/browse/TP-123" suite.previousStatusId = 1 diff --git a/service/incident/impl/incident_service_v2.go b/service/incident/impl/incident_service_v2.go index d2e60fe..c99cfb0 100644 --- a/service/incident/impl/incident_service_v2.go +++ b/service/incident/impl/incident_service_v2.go @@ -12,6 +12,7 @@ import ( "houston/common/metrics" "houston/common/util" houstonSlackUtil "houston/common/util/slack" + stringUtil "houston/common/util/string" "houston/internal/processor/action/view" "houston/logger" "houston/model/incident" @@ -21,6 +22,7 @@ import ( "houston/model/product" "houston/model/severity" "houston/model/team" + teamUserModel "houston/model/teamUser" "houston/model/user" "houston/pkg/atlassian" "houston/pkg/atlassian/dto/response" @@ -175,7 +177,7 @@ func (i *IncidentServiceV2) CreateIncident( emptyResponse := service.IncidentResponse{} // Create incident dto logger.Info(fmt.Sprintf("%s received request to create incident: %+v", logTag, request)) - teamEntity, severityEntity, err := getTeamAndSeverityEntity(i, request.TeamID, request.SeverityID) + responderTeam, severityEntity, err := getTeamAndSeverityEntity(i, request.TeamID, request.SeverityID) if err != nil { return emptyResponse, err } @@ -231,7 +233,7 @@ func (i *IncidentServiceV2) CreateIncident( } logger.Info(fmt.Sprintf("%s [%s] Team, Severity and IncidentStatus entity fetched successfully", logTag, incidentName)) - err = i.slackService.SetChannelTopic(channel, teamEntity, severityEntity, incidentEntity) + err = i.slackService.SetChannelTopic(channel, responderTeam, severityEntity, incidentEntity) if err != nil { logger.Error( fmt.Sprintf("%s [%s] Failed to set channel topic", logTag, incidentName), @@ -241,7 +243,15 @@ func (i *IncidentServiceV2) CreateIncident( logger.Info(fmt.Sprintf("%s [%s] Channel topic is set", logTag, incidentName)) go func() { - err := createIncidentWorkflow(i, channel, incidentEntity, teamEntity, severityEntity, incidentStatusEntity, blazeGroupChannelID) + err := createIncidentWorkflow( + i, + channel, + incidentEntity, + responderTeam, + severityEntity, + incidentStatusEntity, + blazeGroupChannelID, + ) if err != nil { metrics.PublishHoustonFlowFailureMetrics(CREATE_INCIDENT, err.Error()) return @@ -249,7 +259,7 @@ func (i *IncidentServiceV2) CreateIncident( i.HandleKrakatoaWorkflow(incidentEntity) }() - go postInWebhookSlackChannel(i, teamEntity, incidentEntity, severityEntity) + go postInWebhookSlackChannel(i, responderTeam, incidentEntity, severityEntity) go i.SendAlert(incidentEntity) return service.ConvertToIncidentResponse(*incidentEntity), nil @@ -257,10 +267,16 @@ func (i *IncidentServiceV2) CreateIncident( func (i *IncidentServiceV2) PostIncidentCreationWorkflow( channel *slackClient.Channel, - incidentEntity *incident.IncidentEntity, - teamID, severityID, blazeGroupChannelID string, + incidentID uint, + blazeGroupChannelID string, ) { - teamEntity, severityEntity, err := getTeamAndSeverityEntity(i, teamID, severityID) + incidentEntity, err := i.incidentRepository.FindIncidentById(incidentID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get incident by id", logTag), zap.Error(err)) + } + responderTeamID := fmt.Sprintf("%d", incidentEntity.TeamId) + severityID := fmt.Sprintf("%d", incidentEntity.SeverityId) + responderTeam, severityEntity, err := getTeamAndSeverityEntity(i, responderTeamID, severityID) if err != nil { logger.Error(fmt.Sprintf("%s failed to get team and severity entity", logTag), zap.Error(err)) } @@ -268,14 +284,22 @@ func (i *IncidentServiceV2) PostIncidentCreationWorkflow( if err != nil { logger.Error(fmt.Sprintf("%s failed to get incident status entity", logTag), zap.Error(err)) } - err = createIncidentWorkflow(i, channel, incidentEntity, teamEntity, severityEntity, incidentStatusEntity, blazeGroupChannelID) + err = createIncidentWorkflow( + i, + channel, + incidentEntity, + responderTeam, + severityEntity, + incidentStatusEntity, + blazeGroupChannelID, + ) if err != nil { metrics.PublishHoustonFlowFailureMetrics(CREATE_INCIDENT, err.Error()) return } i.HandleKrakatoaWorkflow(incidentEntity) - go postInWebhookSlackChannel(i, teamEntity, incidentEntity, severityEntity) + go postInWebhookSlackChannel(i, responderTeam, incidentEntity, severityEntity) go i.SendAlert(incidentEntity) } @@ -632,16 +656,26 @@ func createIncidentWorkflow( i *IncidentServiceV2, channel *slackClient.Channel, incidentEntity *incident.IncidentEntity, - teamEntity *team.TeamEntity, + responderTeam *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity, blazeGroupChannelID string, ) error { incidentName := incidentEntity.IncidentName - _, err := postIncidentSummary( + var err error + var reportingTeam *team.TeamEntity + if incidentEntity.ReportingTeamId != nil { + reportingTeam, err = i.teamRepository.FindTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("%s [%s] failed to get reporting team", logTag, incidentName), zap.Error(err)) + return err + } + } + _, err = postIncidentSummary( channel.ID, incidentEntity, - teamEntity, + reportingTeam, + responderTeam, severityEntity, incidentStatusEntity, i.incidentRepository, @@ -677,11 +711,11 @@ func createIncidentWorkflow( logger.Info(fmt.Sprintf("%s [%s] Incident creator is added to the slack channel", logTag, incidentName)) } // Call addMembersToIncident(), provide channel, team name and severity - err = addMembersToIncident(channel, i, teamEntity, severityEntity, incidentName) + err = addMembersToIncident(channel, i, reportingTeam, responderTeam, severityEntity, incidentName) // Tag oncall // Call slack service to tag the oncall, provide incident id and slack id to be tagged - tagPseOrDevOncallToIncident(channel, severityEntity, teamEntity, i) + tagPseOrDevOncallToIncident(channel, severityEntity, responderTeam, i) logger.Info(fmt.Sprintf("%s [%s] oncall us tagged to the incident", logTag, incidentName)) // Assign responder @@ -690,7 +724,7 @@ func createIncidentWorkflow( err = assignResponderToIncident( i, incidentEntity, - teamEntity, + responderTeam, severityEntity, incidentName, ) @@ -766,6 +800,7 @@ Builds incident summary message blocks and posts incident summary to Blaze Group func postIncidentSummary( incidentChannelID string, incidentEntity *incident.IncidentEntity, + reportingTeamEntity *team.TeamEntity, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity, @@ -773,7 +808,9 @@ func postIncidentSummary( blazeGroupChannelID string, slackService slack.ISlackService, ) (*string, error) { - blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + blocks := view.IncidentSummarySectionV3( + incidentEntity, reportingTeamEntity, teamEntity, severityEntity, incidentStatusEntity, + ) color := util.GetColorBySeverity(incidentEntity.SeverityId) if blazeGroupChannelID != "" { timestamp, err := slackService.PostMessageBlocks(blazeGroupChannelID, blocks, color) @@ -830,7 +867,8 @@ checks for active houston users and posts appropriate messages for deactivated u func addMembersToIncident( channel *slackClient.Channel, i *IncidentServiceV2, - teamEntity *team.TeamEntity, + reportingTeamEntity *team.TeamEntity, + responderTeamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentName string, ) error { @@ -838,31 +876,15 @@ func addMembersToIncident( var houstonUserList []string var notHoustonUserList []string - if teamEntity.OncallHandle != "" { - allUserToBeAddedIntoIncident = append(allUserToBeAddedIntoIncident, teamEntity.OncallHandle) + if reportingTeamEntity != nil { + updateTeamMembersListToBeAddedIntoIncident( + i, reportingTeamEntity, severityEntity, &allUserToBeAddedIntoIncident, &houstonUserList, ¬HoustonUserList, + ) } - if teamEntity.PseOncallHandle != "" { - allUserToBeAddedIntoIncident = append(allUserToBeAddedIntoIncident, teamEntity.PseOncallHandle) - } - - if len(severityEntity.SlackUserIds) > 0 { - allUserToBeAddedIntoIncident = append(allUserToBeAddedIntoIncident, severityEntity.SlackUserIds...) - } - - teamUsers, err := i.teamUserService.GetTeamUsersWithMinimumSeverityIdLessThanOrEqualToGivenSeverity(teamEntity.ID, severityEntity.ID) - if err != nil { - return err - } - for _, teamUserDTO := range teamUsers { - if teamUserDTO.User.Active { - houstonUserList = append(houstonUserList, teamUserDTO.User.SlackUserId) - } else { - notHoustonUserList = append(notHoustonUserList, teamUserDTO.User.SlackUserId) - } - } - - allUserToBeAddedIntoIncident = append(allUserToBeAddedIntoIncident, houstonUserList...) + updateTeamMembersListToBeAddedIntoIncident( + i, responderTeamEntity, severityEntity, &allUserToBeAddedIntoIncident, &houstonUserList, ¬HoustonUserList, + ) uniqueUsersToBeAdded := util.RemoveDuplicate[string](allUserToBeAddedIntoIncident) @@ -891,7 +913,7 @@ func addMembersToIncident( _ = err } - err = i.slackService.InviteUsersToConversation(channel.ID, uniqueUsersToBeAdded[:]...) + err := i.slackService.InviteUsersToConversation(channel.ID, uniqueUsersToBeAdded[:]...) if err != nil { logger.Error( fmt.Sprintf( @@ -907,6 +929,41 @@ func addMembersToIncident( return nil } +func updateTeamMembersListToBeAddedIntoIncident( + i *IncidentServiceV2, + teamEntity *team.TeamEntity, + severityEntity *severity.SeverityEntity, + allUserToBeAddedIntoIncident, houstonUserList, notHoustonUserList *[]string, +) { + if teamEntity.OncallHandle != "" { + *allUserToBeAddedIntoIncident = append(*allUserToBeAddedIntoIncident, teamEntity.OncallHandle) + } + + if teamEntity.PseOncallHandle != "" { + *allUserToBeAddedIntoIncident = append(*allUserToBeAddedIntoIncident, teamEntity.PseOncallHandle) + } + + if len(severityEntity.SlackUserIds) > 0 { + *allUserToBeAddedIntoIncident = append(*allUserToBeAddedIntoIncident, severityEntity.SlackUserIds...) + } + + teamUsers, err := i.teamUserService.GetTeamUsersWithMinimumSeverityIdLessThanOrEqualToGivenSeverity( + teamEntity.ID, severityEntity.ID, + ) + if err != nil { + logger.Error("Error in getting team users", zap.Error(err)) + } + for _, teamUserDTO := range teamUsers { + if teamUserDTO.User.Active { + *houstonUserList = append(*houstonUserList, teamUserDTO.User.SlackUserId) + } else { + *notHoustonUserList = append(*notHoustonUserList, teamUserDTO.User.SlackUserId) + } + } + + *allUserToBeAddedIntoIncident = append(*allUserToBeAddedIntoIncident, *houstonUserList...) +} + /* gets incident, team, severity as input finds the responder, updates it in DB, and posts the message in incident slack channel @@ -1172,24 +1229,33 @@ func processUpdateMessage( teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, i *IncidentServiceV2, ) []error { - var errors []error - blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + var errs []error + var reportingTeamEntity *team.TeamEntity = nil + if incidentEntity.ReportingTeamId != nil { + var err error + reportingTeamEntity, err = i.teamRepository.FindTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get reporting team", updateLogTag), zap.Error(err)) + errs = append(errs, err) + } + } + blocks := view.IncidentSummarySectionV3(incidentEntity, reportingTeamEntity, teamEntity, severityEntity, incidentStatusEntity) color := util.GetColorBySeverity(severityEntity.ID) att := slackUtil.Attachment{Blocks: blocks, Color: color} - for _, message := range *incidentChannels { + for _, message := range incidentChannels { err := i.slackService.UpdateMessageWithAttachments(message.SlackChannel, message.MessageTimeStamp, att) if err != nil { logger.Error(fmt.Sprintf("exception occurred while updating the message to all the incident "+ "channels for incidentId: %v", incidentEntity.ID), zap.Error(err)) - errors = append(errors, err) + errs = append(errs, err) } } - return errors + return errs } /* @@ -1225,7 +1291,7 @@ func (i *IncidentServiceV2) FetchAllUpdateIncidentData(incidentId uint, userEmai *team.TeamEntity, *severity.SeverityEntity, *incident.IncidentStatusEntity, - *[]incident_channel.IncidentChannelEntity, + []incident_channel.IncidentChannelEntity, error, ) { incidentEntity, err := i.incidentRepository.FindIncidentById(incidentId) @@ -1284,7 +1350,7 @@ func (i *IncidentServiceV2) FetchAllEntitiesForIncident(incidentEntity *incident *team.TeamEntity, *severity.SeverityEntity, *incident.IncidentStatusEntity, - *[]incident_channel.IncidentChannelEntity, + []incident_channel.IncidentChannelEntity, error, ) { teamEntity, severityEntity, err := getTeamAndSeverityEntity( @@ -1388,7 +1454,7 @@ func (i *IncidentServiceV2) UpdateSeverityId( incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, incidentStatusEntity *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { if !util.IsBlank(request.SeverityId) { num, err := strconv.ParseUint(request.SeverityId, 10, 64) @@ -1475,11 +1541,11 @@ func (i *IncidentServiceV2) UpdateSeverityWorkflow( teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { var slackErrors []error - err := i.AddUsersToIncidentUponSeverityUpdate(incidentEntity.SlackChannel, teamEntity, severityEntity) + err := i.AddUsersToIncidentUponSeverityUpdate(incidentEntity, severityEntity) if err != nil { logger.Error(fmt.Sprintf("%s error while adding default users to incident", updateLogTag), zap.Error(err)) slackErrors = append(slackErrors, err) @@ -1570,7 +1636,7 @@ func (i *IncidentServiceV2) UpdateStatus( incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { if !util.IsBlank(request.Status) { statusID, err := strconv.ParseUint(request.Status, 10, 64) @@ -1658,7 +1724,7 @@ func (i *IncidentServiceV2) UpdateStatusWorkflow( teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatus *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { var waitGroup sync.WaitGroup @@ -1705,7 +1771,7 @@ func (i *IncidentServiceV2) UpdateTeamId( userId string, incidentEntity *incident.IncidentEntity, incidentStatusEntity *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { if !util.IsBlank(request.TeamId) { severityEntity, err := i.severityRepository.FindSeverityById(incidentEntity.SeverityId) @@ -1714,20 +1780,20 @@ func (i *IncidentServiceV2) UpdateTeamId( zap.Any("severity", incidentEntity.SeverityId), zap.Error(err)) return err } - num, err := strconv.ParseUint(request.TeamId, 10, 64) + teamIDToUpdate, err := strconv.ParseUint(request.TeamId, 10, 64) if err != nil { logger.Error(fmt.Sprintf("%s error in string to int conversion", updateLogTag), zap.String("TeamId", request.TeamId), zap.Error(err)) return fmt.Errorf("Invalid team ID: %s", request.TeamId) } - if incidentEntity.TeamId != uint(num) { - teamEntity, err := i.teamRepository.FindTeamById(uint(num)) + if incidentEntity.TeamId != uint(teamIDToUpdate) { + teamEntity, err := i.teamRepository.FindTeamById(uint(teamIDToUpdate)) if err != nil || teamEntity == nil { logger.Error(fmt.Sprintf("%s error in fetching team by id", updateLogTag), zap.String("TeamId", request.TeamId), zap.Error(err)) - return fmt.Errorf("Error in fetching team by ID: %s", request.TeamId) + return fmt.Errorf("error in fetching team by ID: %s", request.TeamId) } - incidentEntity.TeamId = uint(num) + incidentEntity.TeamId = uint(teamIDToUpdate) err = i.commitIncidentEntity(incidentEntity, userId) if err != nil { @@ -1736,7 +1802,7 @@ func (i *IncidentServiceV2) UpdateTeamId( } err = i.UpdateTeamIdWorkflow( - userId, incidentEntity, teamEntity, severityEntity, incidentStatusEntity, incidentChannels, + userId, incidentEntity.ID, teamEntity, severityEntity, incidentStatusEntity, incidentChannels, ) if err != nil { logger.Error(fmt.Sprintf("%s error in update team id workflow", updateLogTag), zap.Error(err)) @@ -1752,24 +1818,24 @@ func (i *IncidentServiceV2) UpdateTeamId( func (i *IncidentServiceV2) updateTeamInIncidentEntity( request request.UpdateIncidentRequest, incidentEntity *incident.IncidentEntity, + isTeamUpdateRequired bool, ) (*team.TeamEntity, error) { - teamID, err := strconv.ParseUint(request.TeamId, 10, 64) + teamID, err := stringUtil.StringToUint(request.TeamId) if err != nil { logger.Error(fmt.Sprintf("%s error in string to int conversion", updateLogTag), zap.String("TeamId", request.TeamId), zap.Error(err)) return nil, fmt.Errorf("invalid team ID: %s", request.TeamId) } - if incidentEntity.TeamId == uint(teamID) { - return nil, fmt.Errorf("team ID is same as existing team ID. No update required") - } - teamEntity, err := i.teamRepository.FindTeamById(uint(teamID)) + teamEntity, err := i.teamRepository.FindTeamById(teamID) if err != nil || teamEntity == nil { logger.Error(fmt.Sprintf("%s error in fetching team by id", updateLogTag), zap.String("TeamId", request.TeamId), zap.Error(err)) return nil, fmt.Errorf("error in fetching team by ID: %s", request.TeamId) } - incidentEntity.TeamId = uint(teamID) + if isTeamUpdateRequired { + incidentEntity.TeamId = teamID + } return teamEntity, nil } @@ -1779,11 +1845,11 @@ func (i *IncidentServiceV2) postTeamUpdateFlows( incidentEntity *incident.IncidentEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, teamEntity *team.TeamEntity, ) error { err := i.UpdateTeamIdWorkflow( - userId, incidentEntity, teamEntity, severityEntity, incidentStatusEntity, incidentChannels, + userId, incidentEntity.ID, teamEntity, severityEntity, incidentStatusEntity, incidentChannels, ) if err != nil { logger.Error(fmt.Sprintf("%s error in update team id workflow", updateLogTag), zap.Error(err)) @@ -1794,13 +1860,31 @@ func (i *IncidentServiceV2) postTeamUpdateFlows( return nil } +func (i *IncidentServiceV2) postProductUpdateFlows( + userId string, + incidentEntity *incident.IncidentEntity, + severityEntity *severity.SeverityEntity, + incidentStatusEntity *incident.IncidentStatusEntity, + incidentChannels []incident_channel.IncidentChannelEntity, + teamEntity *team.TeamEntity, +) error { + err := i.UpdateProductIdWorkflow( + userId, incidentEntity.ID, teamEntity, severityEntity, incidentStatusEntity, incidentChannels, + ) + if err != nil { + logger.Error(fmt.Sprintf("%s error in update team id workflow", updateLogTag), zap.Error(err)) + return err + } + return nil +} + func (i *IncidentServiceV2) UpdateProductID( request request.UpdateIncidentRequest, userId string, incidentEntity *incident.IncidentEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { var existingProductsIds []uint for _, productEntity := range incidentEntity.Products { @@ -1824,12 +1908,10 @@ func (i *IncidentServiceV2) UpdateProductID( var teamEntity *team.TeamEntity var err error - if isTeamUpdateRequired { - teamEntity, err = i.updateTeamInIncidentEntity(request, incidentEntity) - if err != nil { - logger.Error(fmt.Sprintf("%s error in updating team id in incident entity", updateLogTag), zap.Error(err)) - return err - } + teamEntity, err = i.updateTeamInIncidentEntity(request, incidentEntity, isTeamUpdateRequired) + if err != nil { + logger.Error(fmt.Sprintf("%s error in updating team id in incident entity", updateLogTag), zap.Error(err)) + return err } err = i.commitIncidentEntityWithAssociations(incidentEntity, userId) @@ -1839,11 +1921,17 @@ func (i *IncidentServiceV2) UpdateProductID( } if isTeamUpdateRequired { - err := i.postTeamUpdateFlows(userId, incidentEntity, severityEntity, incidentStatusEntity, incidentChannels, teamEntity) + err = i.postTeamUpdateFlows(userId, incidentEntity, severityEntity, incidentStatusEntity, incidentChannels, teamEntity) if err != nil { logger.Error(fmt.Sprintf("%s error in running post team update workflow", updateLogTag), zap.Error(err)) return err } + } else { + err := i.postProductUpdateFlows(userId, incidentEntity, severityEntity, incidentStatusEntity, incidentChannels, teamEntity) + if err != nil { + logger.Error(fmt.Sprintf("%s error in running post product update workflow", updateLogTag), zap.Error(err)) + return err + } } return nil @@ -1857,15 +1945,21 @@ updates incident card, sets channel topic, throws first encountered error */ func (i *IncidentServiceV2) UpdateTeamIdWorkflow( userId string, - incidentEntity *incident.IncidentEntity, + incidentID uint, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatus *incident.IncidentStatusEntity, - incidentChannels *[]incident_channel.IncidentChannelEntity, + incidentChannels []incident_channel.IncidentChannelEntity, ) error { var slackErrors []error - err := i.AddUsersToIncidentUponTeamUpdate(incidentEntity.SlackChannel, teamEntity, severityEntity) + incidentEntity, err := i.incidentRepository.FindIncidentById(incidentID) + if err != nil { + logger.Error(fmt.Sprintf("%s error in fetching incident by id", updateLogTag), zap.Error(err)) + return err + } + + err = i.AddUsersToIncidentUponTeamUpdate(incidentEntity, teamEntity, severityEntity) if err != nil { logger.Error( fmt.Sprintf("%s error while adding default users to incident", updateLogTag), zap.Error(err), @@ -1955,6 +2049,56 @@ func (i *IncidentServiceV2) UpdateTeamIdWorkflow( return nil } +func (i *IncidentServiceV2) UpdateProductIdWorkflow( + userId string, + incidentID uint, + teamEntity *team.TeamEntity, + severityEntity *severity.SeverityEntity, + incidentStatus *incident.IncidentStatusEntity, + incidentChannels []incident_channel.IncidentChannelEntity, +) error { + var slackErrors []error + var waitGroup sync.WaitGroup + waitGroup.Add(2) + + incidentEntity, err := i.incidentRepository.FindIncidentById(incidentID) + if err != nil { + logger.Error(fmt.Sprintf("%s error in fetching incident by id", updateLogTag), zap.Error(err)) + return err + } + + var productNames []string + for _, p := range incidentEntity.Products { + productNames = append(productNames, p.Name) + } + + go util.ExecuteConcurrentAction(&waitGroup, func() { + txt := fmt.Sprintf( + "<@%s> updated incident products to: *%s*", + userId, + strings.Join(productNames, ", "), + ) + _, err := i.slackService.PostMessageByChannelID(txt, false, incidentEntity.SlackChannel) + if err != nil { + logger.Error( + fmt.Sprintf("%s post response failed for IncidentUpdateType", updateLogTag), zap.Error(err), + ) + slackErrors = append(slackErrors, err) + } + }) + + go util.ExecuteConcurrentAction(&waitGroup, func() { + processUpdateMessage(incidentEntity, teamEntity, severityEntity, incidentStatus, incidentChannels, i) + }) + + waitGroup.Wait() + if slackErrors != nil && len(slackErrors) != 0 { + return slackErrors[0] + } + + return nil +} + /* gets update incident request and required entities checks for metadata validation, performs DB update and posts updated metadata @@ -1964,14 +2108,14 @@ func (i *IncidentServiceV2) UpdateMetaData( incidentEntity *incident.IncidentEntity, userId string, ) error { - metaData := incidentRequest.CreateIncidentMetadata{} - if updateIncidentRequest.MetaData != metaData { - metaData.CrmTicketCreationTime = updateIncidentRequest.MetaData.CrmTicketCreationTime - metaData.CustomerId = updateIncidentRequest.MetaData.CustomerId - metaData.PhoneNumber = updateIncidentRequest.MetaData.PhoneNumber - metaData.TicketId = updateIncidentRequest.MetaData.TicketId - metaData.AgentName = updateIncidentRequest.MetaData.AgentName - metaData.TicketGroup = updateIncidentRequest.MetaData.TicketGroup + metadata := &incidentRequest.CreateIncidentMetadata{} + if reflect.DeepEqual(updateIncidentRequest.MetaData, metadata) { + metadata.CrmTicketCreationTime = updateIncidentRequest.MetaData.CrmTicketCreationTime + metadata.CustomerId = updateIncidentRequest.MetaData.CustomerId + metadata.PhoneNumber = updateIncidentRequest.MetaData.PhoneNumber + metadata.TicketId = updateIncidentRequest.MetaData.TicketId + metadata.AgentName = updateIncidentRequest.MetaData.AgentName + metadata.TicketGroup = updateIncidentRequest.MetaData.TicketGroup var incidentMetadata []incidentRequest.CreateIncidentMetadata if incidentEntity.MetaData != nil { @@ -1984,7 +2128,7 @@ func (i *IncidentServiceV2) UpdateMetaData( return err } } - incidentMetadata = append(incidentMetadata, metaData) + incidentMetadata = append(incidentMetadata, *metadata) var err error incidentEntity.MetaData, err = json.Marshal(incidentMetadata) if err != nil { @@ -2001,14 +2145,14 @@ func (i *IncidentServiceV2) UpdateMetaData( return err } - marshalledMetadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{updateIncidentRequest.MetaData}) + marshalledMetadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*updateIncidentRequest.MetaData}) msgOption, err := houstonSlackUtil.BuildSlackTextMessageFromMetaData(marshalledMetadata, true) if err != nil { logger.Error( fmt.Sprintf( "%s build slack text message failed for IncidentUpdateCustomerData: %v", updateLogTag, - metaData, + metadata, ), zap.Error(err), ) @@ -2034,18 +2178,27 @@ adds the members belonging to the respective team and severity to the channel pr */ func (i *IncidentServiceV2) AddUsersToIncidentUponSeverityUpdate( - channelId string, - teamEntity *team.TeamEntity, - severityEntity *severity.SeverityEntity) error { + incidentEntity *incident.IncidentEntity, severityEntity *severity.SeverityEntity, +) error { var userIdList []string + channelId := incidentEntity.SlackChannel + var allTeamMembers []teamUserModel.TeamUserDTO - teamUsers, err := i.teamUserService.GetTeamUsersForGivenSeverity(teamEntity.ID, severityEntity.ID) + if incidentEntity.ReportingTeamId != nil { + reportingTeamMembers, err := i.teamUserService.GetTeamUsersForGivenSeverity(*incidentEntity.ReportingTeamId, severityEntity.ID) + if err != nil { + return err + } + allTeamMembers = append(allTeamMembers, reportingTeamMembers...) + } + responderTeamUsers, err := i.teamUserService.GetTeamUsersForGivenSeverity(incidentEntity.TeamId, severityEntity.ID) if err != nil { return err } + allTeamMembers = append(allTeamMembers, responderTeamUsers...) - if len(teamUsers) != 0 { - for _, teamUserDTO := range teamUsers { + if len(allTeamMembers) != 0 { + for _, teamUserDTO := range allTeamMembers { userIdList = append(userIdList, teamUserDTO.User.SlackUserId) } } @@ -2060,31 +2213,27 @@ func (i *IncidentServiceV2) AddUsersToIncidentUponSeverityUpdate( } func (i *IncidentServiceV2) AddUsersToIncidentUponTeamUpdate( - channelId string, + incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity) error { - var userIdList []string - teamUsers, err := i.teamUserService.GetTeamUsersWithMinimumSeverityIdLessThanOrEqualToGivenSeverity(teamEntity.ID, severityEntity.ID) + if incidentEntity.ReportingTeamId != nil { + reportingTeamMembers, err := i.teamUserService.GetTeamUsersWithMinimumSeverityIdLessThanOrEqualToGivenSeverity( + *incidentEntity.ReportingTeamId, severityEntity.ID, + ) + if err != nil { + return err + } + i.addTeamUsersToIncident(incidentEntity.SlackChannel, reportingTeamMembers, teamEntity) + } + + responderTeamUsers, err := i.teamUserService.GetTeamUsersWithMinimumSeverityIdLessThanOrEqualToGivenSeverity( + incidentEntity.TeamId, severityEntity.ID, + ) if err != nil { return err } - - if teamUsers != nil && len(teamUsers) != 0 { - for _, teamUserDTO := range teamUsers { - userIdList = append(userIdList, teamUserDTO.User.SlackUserId) - } - } - - if !util.IsBlank(teamEntity.OncallHandle) { - userIdList = append(userIdList, teamEntity.OncallHandle) - } - - if !util.IsBlank(teamEntity.PseOncallHandle) { - userIdList = append(userIdList, teamEntity.PseOncallHandle) - } - - i.addUsersToIncident(channelId, userIdList) + i.addTeamUsersToIncident(incidentEntity.SlackChannel, responderTeamUsers, teamEntity) return nil } @@ -2107,6 +2256,27 @@ func (i *IncidentServiceV2) addUsersToIncident(channelId string, userIdList []st } } +func (i *IncidentServiceV2) addTeamUsersToIncident( + channelId string, teamUsers []teamUserModel.TeamUserDTO, teamEntity *team.TeamEntity, +) { + var userIdList []string + if teamUsers != nil && len(teamUsers) != 0 { + for _, teamUserDTO := range teamUsers { + userIdList = append(userIdList, teamUserDTO.User.SlackUserId) + } + } + + if !util.IsBlank(teamEntity.OncallHandle) { + userIdList = append(userIdList, teamEntity.OncallHandle) + } + + if !util.IsBlank(teamEntity.PseOncallHandle) { + userIdList = append(userIdList, teamEntity.PseOncallHandle) + } + + i.addUsersToIncident(channelId, userIdList) +} + func (i *IncidentServiceV2) createConferenceAndPostMessageInSlack(incidentEntity *incident.IncidentEntity) { incidentName := incidentEntity.IncidentName if viper.GetBool("ENABLE_CONFERENCE") { diff --git a/service/incident/incident_service_v2_interface.go b/service/incident/incident_service_v2_interface.go index 8d57626..87bb611 100644 --- a/service/incident/incident_service_v2_interface.go +++ b/service/incident/incident_service_v2_interface.go @@ -15,8 +15,8 @@ type IIncidentService interface { CreateIncident(request incidentRequest.CreateIncidentRequestV2, source string, blazeGroupChannelID string) (service.IncidentResponse, error) PostIncidentCreationWorkflow( channel *slackClient.Channel, - incidentEntity *incident.IncidentEntity, - teamID, severityID, blazeGroupChannelID string, + incidentID uint, + blazeGroupChannelID string, ) GetIncidentById(incidentId uint) (*incident.IncidentEntity, error) GetIncidentByChannelID(channelID string) (*incident.IncidentEntity, error) diff --git a/service/incident_channel/incident_channel_service.go b/service/incident_channel/incident_channel_service.go index a67c40e..2d1a186 100644 --- a/service/incident_channel/incident_channel_service.go +++ b/service/incident_channel/incident_channel_service.go @@ -26,7 +26,7 @@ returns all the incident channels for the given incident id */ func (i *IncidentChannelService) GetIncidentChannels( incidentID uint, -) (*[]incident_channel.IncidentChannelEntity, error) { +) ([]incident_channel.IncidentChannelEntity, error) { incidentChannelEntities, err := i.incidentChannelRepository.GetIncidentChannels(incidentID) if err != nil { logger.Error( diff --git a/service/incident_channel/incident_channel_service_interface.go b/service/incident_channel/incident_channel_service_interface.go index 28e49a2..fef8bc8 100644 --- a/service/incident_channel/incident_channel_service_interface.go +++ b/service/incident_channel/incident_channel_service_interface.go @@ -3,5 +3,5 @@ package incident_channel import "houston/model/incident_channel" type IIncidentChannelService interface { - GetIncidentChannels(incidentID uint) (*[]incident_channel.IncidentChannelEntity, error) + GetIncidentChannels(incidentID uint) ([]incident_channel.IncidentChannelEntity, error) } diff --git a/service/incident_service.go b/service/incident_service.go index ff1472f..ed23ec7 100644 --- a/service/incident_service.go +++ b/service/incident_service.go @@ -12,6 +12,7 @@ import ( logger "houston/logger" "houston/model/incident" "houston/model/log" + "houston/model/product" "houston/model/severity" "houston/model/team" "houston/model/user" @@ -24,9 +25,11 @@ import ( service "houston/service/response" common "houston/service/response/common" slack2 "houston/service/slack" + "houston/service/teamService" utils "houston/service/utils" "math" "net/http" + "reflect" "strconv" "strings" "time" @@ -45,6 +48,7 @@ type incidentService struct { db *gorm.DB socketModeClient *socketmode.Client teamRepository *team.Repository + teamService teamService.ITeamServiceV2 severityRepository *severity.Repository incidentRepository *incident.Repository userRepository *user.Repository @@ -55,7 +59,9 @@ type incidentService struct { rcaService *rcaService.RcaService } -func NewIncidentService(gin *gin.Engine, db *gorm.DB, socketModeClient *socketmode.Client) *incidentService { +func NewIncidentService( + gin *gin.Engine, db *gorm.DB, socketModeClient *socketmode.Client, teamService teamService.ITeamServiceV2, +) *incidentService { severityRepository := severity.NewSeverityRepository(db) logRepository := log.NewLogRepository(db) teamRepository := team.NewTeamRepository(db, logRepository) @@ -71,6 +77,7 @@ func NewIncidentService(gin *gin.Engine, db *gorm.DB, socketModeClient *socketmo db: db, socketModeClient: socketModeClient, teamRepository: teamRepository, + teamService: teamService, severityRepository: severityRepository, incidentRepository: incidentRepository, userRepository: userRepository, @@ -155,8 +162,11 @@ func (i *incidentService) GetIncidents(c *gin.Context) { } func (i *incidentService) GetIncidentResponseFromIncidentEntity( - incidents []incident.IncidentEntity, incidentRepository *incident.Repository, severityRepository *severity.Repository, - teamRepository *team.Repository) ([]service.IncidentResponse, error) { + incidents []incident.IncidentEntity, + incidentRepository *incident.Repository, + severityRepository *severity.Repository, + teamRepository *team.Repository, +) ([]service.IncidentResponse, error) { teams, err := teamRepository.GetAllActiveTeams() if err != nil { @@ -182,9 +192,24 @@ func (i *incidentService) GetIncidentResponseFromIncidentEntity( return nil, err } - var incidentResponses []service.IncidentResponse = []service.IncidentResponse{} + var incidentResponses []service.IncidentResponse for incidentIndex := range incidents { incidentResponse, _ := i.rcaService.GetIncidentResponseWithRCALink(&incidents[incidentIndex]) + var reportingTeamResponse *service.IncidentHeaderOption = nil + if incidents[incidentIndex].ReportingTeamId != nil { + reportingTeamEntity, err := i.teamService.GetTeamById(*incidents[incidentIndex].ReportingTeamId) + if err != nil { + logger.Error("error in fetching reporting team", zap.Error(err)) + return nil, err + } + reportingTeamResponse = teamDTOToIncidentHeaderOption(reportingTeamEntity.ToDTO()) + } + incidentResponse.ReportingTeam = reportingTeamResponse + var products []product.ProductDTO + for _, productEntity := range incidents[incidentIndex].Products { + products = append(products, *productEntity.ToDTO()) + } + incidentResponse.Products = productDTOToIncidentHeaderOption(products) incidentResponses = append(incidentResponses, incidentResponse) for _, t := range *teams { if t.ID == incidents[incidentIndex].TeamId { @@ -483,7 +508,17 @@ func (i *incidentService) postIncidentSummary(incidentChannelID string, incidentEntity *incident.IncidentEntity, teamEntity *team.TeamEntity, severityEntity *severity.SeverityEntity, incidentStatusEntity *incident.IncidentStatusEntity) (*string, error) { // Post incident summary to Blaze Group channel and incident channel - blocks := view.IncidentSummarySection(incidentEntity, teamEntity, severityEntity, incidentStatusEntity) + var reportingTeamEntity *team.TeamEntity = nil + if incidentEntity.ReportingTeamId != nil { + var err error + reportingTeamEntity, err = i.teamRepository.FindTeamById(*incidentEntity.ReportingTeamId) + if err != nil { + logger.Error(fmt.Sprintf("failed to get reporting team"), zap.Error(err)) + } + } + blocks := view.IncidentSummarySectionV3( + incidentEntity, reportingTeamEntity, teamEntity, severityEntity, incidentStatusEntity, + ) color := util.GetColorBySeverity(incidentEntity.SeverityId) att := slack.Attachment{Blocks: blocks, Color: color} _, timestamp, err := i.socketModeClient.PostMessage(incidentChannelID, slack.MsgOptionAttachments(att)) @@ -773,14 +808,14 @@ func (i *incidentService) UpdateTeamId( } func (i *incidentService) UpdateMetaData(updateIncidentRequest request.UpdateIncidentRequest, incidentEntity *incident.IncidentEntity, userId string) error { - metaData := incidentRequest.CreateIncidentMetadata{} - if updateIncidentRequest.MetaData != metaData { - metaData.CrmTicketCreationTime = updateIncidentRequest.MetaData.CrmTicketCreationTime - metaData.CustomerId = updateIncidentRequest.MetaData.CustomerId - metaData.PhoneNumber = updateIncidentRequest.MetaData.PhoneNumber - metaData.TicketId = updateIncidentRequest.MetaData.TicketId - metaData.AgentName = updateIncidentRequest.MetaData.AgentName - metaData.TicketGroup = updateIncidentRequest.MetaData.TicketGroup + metadata := incidentRequest.CreateIncidentMetadata{} + if !reflect.DeepEqual(updateIncidentRequest.MetaData, metadata) { + metadata.CrmTicketCreationTime = updateIncidentRequest.MetaData.CrmTicketCreationTime + metadata.CustomerId = updateIncidentRequest.MetaData.CustomerId + metadata.PhoneNumber = updateIncidentRequest.MetaData.PhoneNumber + metadata.TicketId = updateIncidentRequest.MetaData.TicketId + metadata.AgentName = updateIncidentRequest.MetaData.AgentName + metadata.TicketGroup = updateIncidentRequest.MetaData.TicketGroup var incidentMetadata []incidentRequest.CreateIncidentMetadata if incidentEntity.MetaData != nil { @@ -790,7 +825,7 @@ func (i *incidentService) UpdateMetaData(updateIncidentRequest request.UpdateInc return err } } - incidentMetadata = append(incidentMetadata, metaData) + incidentMetadata = append(incidentMetadata, metadata) var err error incidentEntity.MetaData, err = json.Marshal(incidentMetadata) if err != nil { @@ -805,3 +840,21 @@ func (i *incidentService) UpdateMetaData(updateIncidentRequest request.UpdateInc } return nil } + +func teamDTOToIncidentHeaderOption(t *team.TeamDTO) *service.IncidentHeaderOption { + return &service.IncidentHeaderOption{ + Value: t.ID, + Label: t.Name, + } +} + +func productDTOToIncidentHeaderOption(products []product.ProductDTO) []service.IncidentHeaderOption { + var productResponses []service.IncidentHeaderOption + for _, p := range products { + productResponses = append(productResponses, service.IncidentHeaderOption{ + Value: p.ProductID, + Label: p.ProductName, + }) + } + return productResponses +} diff --git a/service/orchestration/incident_orchestrator.go b/service/orchestration/incident_orchestrator.go index 97539d2..0ec7ada 100644 --- a/service/orchestration/incident_orchestrator.go +++ b/service/orchestration/incident_orchestrator.go @@ -14,8 +14,14 @@ import ( ) type IncidentOrchestrator interface { - GetProductsOfUser(slackOrEmailID string) (*response.ProductsOfUser, error) - GetAssignerAndResponderTeams(emailID string, productID uint) (*response.AssignerAndResponderTeams, error) + GetProductsOfUserByEmailID(emailID string) (*response.ProductsOfUser, error) + GetProductsOfUserBySlackUserID(slackUserID string) (*response.ProductsOfUser, error) + GetReportingAndResponderTeams(emailID string, productIDs []uint) (*response.ReportingAndResponderTeams, error) + GetReportingAndResponderTeamsBySlackUserId( + slackUserID string, productIDs []uint, + ) (*response.ReportingAndResponderTeams, error) + GetResponderTeamsForUpdate(slackChannelID string, productIDs []uint) *response.ProductAndUserTeams + GetProductsForUpdate(slackChannelID string) *response.ProductsForUpdate CreateIncident( request *incidentRequest.CreateIncidentRequestV3, blazeGroupChannelID string, ) (*response.IncidentResponse, error) diff --git a/service/orchestration/incident_orchestrator_impl.go b/service/orchestration/incident_orchestrator_impl.go index 88c9f9d..c9fe176 100644 --- a/service/orchestration/incident_orchestrator_impl.go +++ b/service/orchestration/incident_orchestrator_impl.go @@ -10,7 +10,9 @@ import ( "houston/common/util" "houston/logger" "houston/model/incident" + "houston/model/product" "houston/model/team" + user2 "houston/model/user" incidentService "houston/service/incident" "houston/service/products" "houston/service/productsTeams" @@ -59,59 +61,138 @@ func newIncidentOrchestratorImpl( } const ( - defaultTeam = "AMC" + defaultTeam = "Others" logTag = "[incident_orchestrator]" ) -func (i *incidentOrchestratorImpl) GetProductsOfUser(emailID string) (*response.ProductsOfUser, error) { - teams := i.getTeamsOfUser(emailID) +func (i *incidentOrchestratorImpl) GetProductsOfUserByEmailID(emailID string) (*response.ProductsOfUser, error) { + return i.getProductsOfTeams(i.getTeamsOfUserByEmailID(emailID)) +} - if teams == nil { - defaultTeam, err := i.getDefaultTeam() - if err != nil { - return nil, err - } - teams = append(teams, *defaultTeam) - } +func (i *incidentOrchestratorImpl) GetProductsOfUserBySlackUserID(slackUserID string) (*response.ProductsOfUser, error) { + return i.getProductsOfTeams(i.getTeamsOfUserBySlackUserID(slackUserID)) +} + +func (i *incidentOrchestratorImpl) getProductsOfTeams(teams []team.TeamDTO) (*response.ProductsOfUser, error) { + var productsOfUser []product.ProductDTO + var err error var teamIDs []uint for _, t := range teams { teamIDs = append(teamIDs, t.ID) } - - productsOfUser, err := i.productTeamService.GetProductsByTeamId(teamIDs...) - if err != nil { - return nil, err - } - + productsOfUser = i.productTeamService.GetProductsByTeamId(teamIDs...) allProducts, err := i.productsService.GetAllProducts() if err != nil { return nil, err } + var defaultProduct *product.ProductDTO = nil + if len(productsOfUser) == 1 { + defaultProduct = &productsOfUser[0] + } + return &response.ProductsOfUser{ - DefaultProduct: productsOfUser[0], + DefaultProduct: defaultProduct, Products: allProducts, }, nil } -func (i *incidentOrchestratorImpl) GetAssignerAndResponderTeams( - emailID string, productID uint, -) (*response.AssignerAndResponderTeams, error) { - assignerTeams := i.getTeamsOfUser(emailID) - if len(assignerTeams) == 0 { +func (i *incidentOrchestratorImpl) GetReportingAndResponderTeams( + emailID string, productIDs []uint, +) (*response.ReportingAndResponderTeams, error) { + reportingTeams := i.getTeamsOfUserByEmailID(emailID) + return i.getReportingAndResponderTeams(reportingTeams, productIDs) +} + +func (i *incidentOrchestratorImpl) GetReportingAndResponderTeamsBySlackUserId( + slackUserID string, productIDs []uint, +) (*response.ReportingAndResponderTeams, error) { + reportingTeams := i.getTeamsOfUserBySlackUserID(slackUserID) + return i.getReportingAndResponderTeams(reportingTeams, productIDs) +} + +func (i *incidentOrchestratorImpl) GetResponderTeamsForUpdate( + slackChannelID string, productIDs []uint, +) *response.ProductAndUserTeams { + logger.Info("[GetResponderTeamsForUpdate] channel id is: " + slackChannelID) + incidentEntity, err := i.incidentService.GetIncidentByChannelID(slackChannelID) + if err != nil { + logger.Error(fmt.Sprintf("%s Error in fetching incident by channel id: %s", logTag, slackChannelID), zap.Error(err)) + return nil + } + teamEntity, err := i.teamService.GetTeamById(incidentEntity.TeamId) + if err != nil { + logger.Error(fmt.Sprintf("%s Error in fetching default team", logTag), zap.Error(err)) + } + if productIDs == nil { + currentProducts := incidentEntity.Products + for _, p := range currentProducts { + productIDs = append(productIDs, p.ID) + } + } + teamsOfProducts, err := i.getTeamsOfProducts(productIDs) + if err != nil { + return nil + } + if ifTeamExists(teamsOfProducts, teamEntity) { + teamsOfProducts.DefaultTeam = teamEntity.ToDTO() + } + + return teamsOfProducts +} + +func (i *incidentOrchestratorImpl) GetProductsForUpdate(slackChannelID string) *response.ProductsForUpdate { + incidentEntity, err := i.incidentService.GetIncidentByChannelID(slackChannelID) + if err != nil { + return nil + } + var currentProducts []product.ProductDTO + for _, p := range incidentEntity.Products { + currentProducts = append(currentProducts, *p.ToDTO()) + } + allProducts, err := i.productsService.GetAllProducts() + if err != nil { + return nil + } + productsForUpdate := &response.ProductsForUpdate{ + DefaultProducts: currentProducts, + Products: allProducts, + } + return productsForUpdate +} + +func ifTeamExists(teamsOfProducts *response.ProductAndUserTeams, teamEntity *team.TeamEntity) bool { + for _, t := range teamsOfProducts.Teams { + if t.ID == teamEntity.ID { + return true + } + } + return false +} + +func (i *incidentOrchestratorImpl) getReportingAndResponderTeams( + teamsOfUser []team.TeamDTO, productIDs []uint, +) (*response.ReportingAndResponderTeams, error) { + if len(teamsOfUser) == 0 { defaultTeam, err := i.getDefaultTeam() if err != nil { return nil, fmt.Errorf("error in fetching default team") } - assignerTeams = append(assignerTeams, *defaultTeam) + teamsOfUser = append(teamsOfUser, *defaultTeam) } - teamOfProduct, err := i.getTeamsOfProduct(productID) + teamsOfProducts, err := i.getTeamsOfProducts(productIDs) if err != nil { return nil, err } - return &response.AssignerAndResponderTeams{ - AssignerTeam: &response.ProductAndUserTeams{DefaultTeam: assignerTeams[0], Teams: assignerTeams}, - ResponderTeam: teamOfProduct, + var defaultSelectedReportingTeam *team.TeamDTO = nil + defaultSelectedReportingTeam = &teamsOfUser[0] + var reportingTeams []team.TeamDTO + reportingTeams = append(reportingTeams, teamsOfUser...) + reportingTeams = append(reportingTeams, teamsOfProducts.Teams...) + reportingTeams = util.RemoveDuplicates(reportingTeams, "ID").([]team.TeamDTO) + return &response.ReportingAndResponderTeams{ + ReportingTeam: &response.ProductAndUserTeams{DefaultTeam: defaultSelectedReportingTeam, Teams: reportingTeams}, + ResponderTeam: teamsOfProducts, }, nil } @@ -146,9 +227,7 @@ func (i *incidentOrchestratorImpl) CreateIncident( go func() { i.incidentService.PostIncidentCreationWorkflow( slackChannel, - incidentEntity, - fmt.Sprintf("%d", incidentEntity.TeamId), - fmt.Sprintf("%d", incidentEntity.SeverityId), + incidentEntity.ID, blazeGroupChannelID, ) }() @@ -156,17 +235,32 @@ func (i *incidentOrchestratorImpl) CreateIncident( return &incidentResponse, nil } -func (i *incidentOrchestratorImpl) getTeamsOfUser(emailID string) []team.TeamDTO { +func (i *incidentOrchestratorImpl) getTeamsOfUserByEmailID(emailID string) []team.TeamDTO { userDTO, err := i.userService.GetHoustonUserByEmailId(emailID) + if err != nil { + logger.Error(fmt.Sprintf("%s Error in fetching user by email id", logTag), zap.Error(err)) + } + return i.getTeamByUserDTO(userDTO) +} + +func (i *incidentOrchestratorImpl) getTeamsOfUserBySlackUserID(slackUserId string) []team.TeamDTO { + userDTO, err := i.userService.GetHoustonUserBySlackUserId(slackUserId) if err != nil { return nil } - var teams = make([]team.TeamDTO, 0) + return i.getTeamByUserDTO(userDTO) +} + +func (i *incidentOrchestratorImpl) getTeamByUserDTO(userDTO *user2.UserDTO) []team.TeamDTO { + if userDTO == nil { + return nil + } teamUserDTO, err := i.teamUserService.GetTeamsByUserId(userDTO.ID) if err != nil { return nil } + var teams []team.TeamDTO if len(teamUserDTO) > 0 { for _, t := range teamUserDTO { teams = append(teams, t.Team) @@ -175,27 +269,48 @@ func (i *incidentOrchestratorImpl) getTeamsOfUser(emailID string) []team.TeamDTO return teams } +func (i *incidentOrchestratorImpl) getTeamsOfProducts(productIDs []uint) (*response.ProductAndUserTeams, error) { + productTeamDTO, err := i.productTeamService.GetProductTeamsByProductIDs(productIDs) + if err != nil { + logger.Error(fmt.Sprintf("%s Error in fetching product teams", logTag), zap.Error(err)) + } + if len(productTeamDTO) == 0 { + return i.defaultProductAndUserTeams(), nil + } + + var allTeams []team.TeamDTO + for _, pt := range productTeamDTO { + allTeams = append(allTeams, pt.Teams...) + } + if len(allTeams) == 0 { + return i.defaultProductAndUserTeams(), nil + } + + allUniqueTeams := util.RemoveDuplicates(allTeams, "ID").([]team.TeamDTO) + var defaultSelectedTeam *team.TeamDTO = nil + if len(allUniqueTeams) == 1 { + defaultSelectedTeam = &allUniqueTeams[0] + } + return &response.ProductAndUserTeams{ + DefaultTeam: defaultSelectedTeam, + Teams: allUniqueTeams, + }, nil +} + +func (i *incidentOrchestratorImpl) defaultProductAndUserTeams() *response.ProductAndUserTeams { + defaultTeam, _ := i.getDefaultTeam() + return &response.ProductAndUserTeams{ + DefaultTeam: defaultTeam, + Teams: []team.TeamDTO{*defaultTeam}, + } +} + func (i *incidentOrchestratorImpl) getDefaultTeam() (*team.TeamDTO, error) { tr, err := i.teamService.GetTeamByName(defaultTeam) if err != nil { return nil, fmt.Errorf("error in fetching default team") } - teamDTO := tr.ToDTO() - return &teamDTO, nil -} - -func (i *incidentOrchestratorImpl) getTeamsOfProduct(productID uint) (*response.ProductAndUserTeams, error) { - productTeamDTO, err := i.productTeamService.GetProductTeamsByProductID(productID) - if err != nil { - return nil, err - } - if err != nil { - return nil, err - } - return &response.ProductAndUserTeams{ - DefaultTeam: productTeamDTO.Teams[0], - Teams: productTeamDTO.Teams, - }, nil + return tr.ToDTO(), nil } func (i *incidentOrchestratorImpl) buildCreateIncidentDTO( @@ -213,12 +328,12 @@ func (i *incidentOrchestratorImpl) buildCreateIncidentDTO( go func() { defer wg.Done() - assignerTeam, err := i.teamService.GetTeamDetails(createIncRequest.AssignerTeamID) + reportingTeam, err := i.teamService.GetTeamDetails(createIncRequest.ReportingTeamID) if err != nil { return } mutex.Lock() - createIncidentDTO.AssignerTeamID = assignerTeam.ID + createIncidentDTO.ReportingTeamID = &reportingTeam.ID mutex.Unlock() }() @@ -283,8 +398,8 @@ func (i *incidentOrchestratorImpl) buildCreateIncidentDTO( createIncidentDTO.EnableReminder = false if createIncRequest.Metadata != nil { - metaData, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*createIncRequest.Metadata}) - createIncidentDTO.MetaData = metaData + metadata, _ := json.Marshal([]incidentRequest.CreateIncidentMetadata{*createIncRequest.Metadata}) + createIncidentDTO.MetaData = metadata } return &createIncidentDTO, channelTopic, nil diff --git a/service/orchestration/incident_orchestrator_test.go b/service/orchestration/incident_orchestrator_test.go index 56a0aea..a44af19 100644 --- a/service/orchestration/incident_orchestrator_test.go +++ b/service/orchestration/incident_orchestrator_test.go @@ -63,60 +63,61 @@ func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetProducts tu = append(tu, teamUser.TeamUserDTO{ID: 1, Team: team.TeamDTO{ID: 1}}) suite.teamUserServiceMock.GetTeamsByUserIdMock.Expect(uint(1)).Return(tu, nil) var p = product.ProductDTO{ProductID: 1, ProductName: "test"} - suite.productTeamsServiceMock.GetProductsByTeamIdMock.Expect(1).Return([]product.ProductDTO{p}, nil) + suite.productTeamsServiceMock.GetProductsByTeamIdMock.Expect(1).Return([]product.ProductDTO{p}) suite.productServiceMock.GetAllProductsMock.Expect().Return([]product.ProductDTO{p}, nil) - _, err := suite.service.GetProductsOfUser("test") + _, err := suite.service.GetProductsOfUserByEmailID("test") if err != nil { suite.Fail("Get Products Of User Failed", err) } } func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetProductsOfUserWithInvalidEmailID() { + var teamIds []uint = nil + suite.productTeamsServiceMock.GetProductsByTeamIdMock.Expect(teamIds...).Return(nil) suite.userServiceMock.GetHoustonUserByEmailIdMock.Expect("invalid").Return(nil, errors.New("invalid email id")) - suite.teamServiceMock.GetTeamByNameMock.Expect("AMC").Return(&team.TeamEntity{Model: gorm.Model{ID: 1}, Name: "AMC"}, nil) var p = product.ProductDTO{ProductID: 1, ProductName: "test"} - suite.productTeamsServiceMock.GetProductsByTeamIdMock.Expect(1).Return([]product.ProductDTO{p}, nil) suite.productServiceMock.GetAllProductsMock.Expect().Return([]product.ProductDTO{p}, nil) - _, err := suite.service.GetProductsOfUser("invalid") + _, err := suite.service.GetProductsOfUserByEmailID("invalid") if err != nil { suite.Fail("Get Products Of User With Invalid UserID Failed", err) } } -func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetAssignerAndResponderTeams() { +func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetReportingAndResponderTeams() { suite.userServiceMock.GetHoustonUserByEmailIdMock.Expect("test").Return(&user.UserDTO{ID: 1}, nil) var tu []teamUser.TeamUserDTO tu = append(tu, teamUser.TeamUserDTO{ID: 1, Team: team.TeamDTO{ID: 1}}) suite.teamUserServiceMock.GetTeamsByUserIdMock.Expect(uint(1)).Return(tu, nil) var p = product.ProductDTO{ProductID: 1, ProductName: "test"} var teams = []team.TeamDTO{{ID: 1, Name: "test"}} - suite.productTeamsServiceMock.GetProductTeamsByProductIDMock.Expect(uint(1)).Return( - &products_teams.ProductTeamsDTO{Product: p, Teams: teams}, nil, + suite.productTeamsServiceMock.GetProductTeamsByProductIDsMock.Expect([]uint{1}).Return( + []products_teams.ProductTeamsDTO{{Product: p, Teams: teams}}, nil, ) - _, err := suite.service.GetAssignerAndResponderTeams("test", 1) + _, err := suite.service.GetReportingAndResponderTeams("test", []uint{1}) if err != nil { - suite.Fail("Get Assigner And Responder Teams Failed", err) + suite.Fail("Get Reporting And Responder Teams Failed", err) } } -func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetAssignerAndResponderTeamsWithInvalidEmailID() { +func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetReportingAndResponderTeamsWithInvalidEmailID() { suite.userServiceMock.GetHoustonUserByEmailIdMock.Expect("invalid").Return(nil, errors.New("invalid email id")) - suite.teamServiceMock.GetTeamByNameMock.Expect("AMC").Return(&team.TeamEntity{Model: gorm.Model{ID: 1}, Name: "AMC"}, nil) - suite.productTeamsServiceMock.GetProductTeamsByProductIDMock.Expect(1).Return(nil, errors.New("invalid product id")) - _, err := suite.service.GetAssignerAndResponderTeams("invalid", 1) - if err == nil { - suite.Fail("Get Assigner And Responder Teams With Invalid EmailID Failed", err) + suite.teamServiceMock.GetTeamByNameMock.Expect("Others").Return(&team.TeamEntity{Model: gorm.Model{ID: 1}, Name: "Others"}, nil) + suite.productTeamsServiceMock.GetProductTeamsByProductIDsMock.Expect([]uint{1}).Return(nil, errors.New("invalid product id")) + _, err := suite.service.GetReportingAndResponderTeams("invalid", []uint{1}) + if err != nil { + suite.Fail("Get Reporting And Responder Teams With Invalid EmailID Failed", err) } } -func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetAssignerAndResponderTeamsWithInvalidProductID() { +func (suite *IncidentOrchestratorSuite) TestIncidentOrchestratorImpl_GetReportingAndResponderTeamsWithInvalidProductID() { suite.userServiceMock.GetHoustonUserByEmailIdMock.Expect("test").Return(&user.UserDTO{ID: 1}, nil) var tu []teamUser.TeamUserDTO tu = append(tu, teamUser.TeamUserDTO{ID: 1, Team: team.TeamDTO{ID: 1}}) suite.teamUserServiceMock.GetTeamsByUserIdMock.Expect(uint(1)).Return(tu, nil) - suite.productTeamsServiceMock.GetProductTeamsByProductIDMock.Expect(1).Return(nil, errors.New("invalid product id")) - _, err := suite.service.GetAssignerAndResponderTeams("test", 1) - if err == nil { - suite.Fail("Get Assigner And Responder Teams With Invalid ProductID Failed", err) + suite.productTeamsServiceMock.GetProductTeamsByProductIDsMock.Expect([]uint{1}).Return(nil, errors.New("invalid product id")) + suite.teamServiceMock.GetTeamByNameMock.Expect("Others").Return(&team.TeamEntity{Model: gorm.Model{ID: 1}, Name: "Others"}, nil) + _, err := suite.service.GetReportingAndResponderTeams("test", []uint{1}) + if err != nil { + suite.Fail("Get Reporting And Responder Teams With Invalid ProductID Failed", err) } } diff --git a/service/products/product_service.go b/service/products/product_service.go index b8421d2..98ae07c 100644 --- a/service/products/product_service.go +++ b/service/products/product_service.go @@ -8,6 +8,7 @@ type ProductService interface { CreateProduct(productName string) (uint, error) UpdateProduct(productDTO *productModel.ProductDTO) error GetAllProducts() ([]productModel.ProductDTO, error) + GetDefaultProduct() (*productModel.ProductDTO, error) GetProductByID(productID uint) (*productModel.ProductDTO, error) GetProductsByIDs(productIDs []uint) ([]productModel.ProductDTO, error) GetProductByName(productName string) (*productModel.ProductDTO, error) diff --git a/service/products/product_service_impl.go b/service/products/product_service_impl.go index afcc7cb..a9acdf4 100644 --- a/service/products/product_service_impl.go +++ b/service/products/product_service_impl.go @@ -14,6 +14,8 @@ type productServiceImpl struct { var processingError = errors.New("error while processing the request") +const defaultProductName = "Others" + func (service *productServiceImpl) CreateProduct(productName string) (uint, error) { existingProduct, err := service.repo.GetProductByName(productName) if err != nil && !errors.Is(gorm.ErrRecordNotFound, err) { @@ -54,6 +56,19 @@ func (service *productServiceImpl) GetAllProducts() ([]productModel.ProductDTO, } return productDTOs, nil } + +func (service *productServiceImpl) GetDefaultProduct() (*productModel.ProductDTO, error) { + productEntities, err := service.repo.GetProductByName(defaultProductName) + if err != nil { + return nil, err + } + productDTO := productModel.ProductDTO{ + ProductID: productEntities.ID, + ProductName: productEntities.Name, + } + return &productDTO, nil +} + func (service *productServiceImpl) GetProductByID(productID uint) (*productModel.ProductDTO, error) { productEntity, err := service.repo.GetProductById(productID) if err != nil { diff --git a/service/productsTeams/products_teams_service.go b/service/productsTeams/products_teams_service.go index b722424..3cd9e72 100644 --- a/service/productsTeams/products_teams_service.go +++ b/service/productsTeams/products_teams_service.go @@ -9,8 +9,8 @@ type ProductTeamsService interface { AddProductTeamMapping(productID, teamID uint) (uint, error) GetAllProductTeamsMapping() ([]products_teams.ProductTeamsMappingDTO, error) GetProductTeamMappingByProductID(productID uint) (*products_teams.ProductTeamsMappingDTO, error) - GetProductTeamsByProductID(productID uint) (*products_teams.ProductTeamsDTO, error) - GetProductsByTeamId(teamIDs ...uint) ([]product.ProductDTO, error) + GetProductTeamsByProductIDs(productIDs []uint) ([]products_teams.ProductTeamsDTO, error) + GetProductsByTeamId(teamIDs ...uint) []product.ProductDTO DeleteProductTeamMapping(productID, teamID uint) error } diff --git a/service/productsTeams/products_teams_service_impl.go b/service/productsTeams/products_teams_service_impl.go index d459712..7a150a0 100644 --- a/service/productsTeams/products_teams_service_impl.go +++ b/service/productsTeams/products_teams_service_impl.go @@ -50,24 +50,34 @@ func (service *productsTeamsServiceImpl) GetProductTeamMappingByProductID( return productTeamsDTO, nil } -func (service *productsTeamsServiceImpl) GetProductTeamsByProductID( - productID uint, -) (*products_teams.ProductTeamsDTO, error) { - productTeamsDTO, err := service.repo.GetProductTeamsByProductID(productID) - if err != nil { - logger.Error( - fmt.Sprintf( - "error while processing the get product team request for product ID: %d. %+v", productID, err, - ), - ) - return nil, processingError +func (service *productsTeamsServiceImpl) GetProductTeamsByProductIDs( + productIDs []uint, +) ([]products_teams.ProductTeamsDTO, error) { + var productTeamsDTOs []products_teams.ProductTeamsDTO + + for _, productID := range productIDs { + productTeamsDTO, err := service.repo.GetProductTeamsByProductID(productID) + if err != nil { + logger.Error( + fmt.Sprintf( + "error while processing the get product team request for product ID: %d. %+v", productID, err, + ), + ) + } + if productTeamsDTO != nil { + productTeamsDTOs = append(productTeamsDTOs, *productTeamsDTO) + } } - return productTeamsDTO, nil + + return productTeamsDTOs, nil } func (service *productsTeamsServiceImpl) GetProductsByTeamId( teamIDs ...uint, -) ([]product.ProductDTO, error) { +) []product.ProductDTO { + if len(teamIDs) == 0 { + return nil + } productDTOs, err := service.repo.GetProductsByTeamID(teamIDs...) if err != nil { logger.Error( @@ -75,9 +85,9 @@ func (service *productsTeamsServiceImpl) GetProductsByTeamId( "error while processing the get product team request for team ID: %d. %+v", teamIDs, err, ), ) - return nil, processingError + return nil } - return productDTOs, nil + return productDTOs } func (service *productsTeamsServiceImpl) DeleteProductTeamMapping(productID, teamID uint) error { diff --git a/service/request/incident/create_incident.go b/service/request/incident/create_incident.go index f6b37bd..87343c5 100644 --- a/service/request/incident/create_incident.go +++ b/service/request/incident/create_incident.go @@ -27,7 +27,7 @@ type CreateIncidentRequestV3 struct { Title string `json:"title"` Description string `json:"description"` SeverityID uint `json:"severityId"` - AssignerTeamID uint `json:"assignerTeamId"` + ReportingTeamID uint `json:"reportingTeamId"` ResponderTeamID uint `json:"responderTeamId"` ProductIds []uint `json:"productIds"` CreatedBy string `json:"createdBy"` diff --git a/service/request/update_incident.go b/service/request/update_incident.go index 37d7ce6..c84e353 100644 --- a/service/request/update_incident.go +++ b/service/request/update_incident.go @@ -3,12 +3,12 @@ package service import "houston/service/request/incident" type UpdateIncidentRequest struct { - Id uint `json:"id"` - Status string `json:"status,omitempty"` - TeamId string `json:"teamId,omitempty"` - SeverityId string `json:"severityId,omitempty"` - MetaData incident.CreateIncidentMetadata `json:"metaData,omitempty"` - DuplicateOfId uint `json:"duplicateOfId,omitempty"` - Justification string `json:"justification,omitempty"` - ProductIDs []uint `json:"productIds,omitempty"` + Id uint `json:"id"` + Status string `json:"status,omitempty"` + TeamId string `json:"teamId,omitempty"` + SeverityId string `json:"severityId,omitempty"` + MetaData *incident.CreateIncidentMetadata `json:"metaData,omitempty"` + DuplicateOfId uint `json:"duplicateOfId,omitempty"` + Justification string `json:"justification,omitempty"` + ProductIDs []uint `json:"productIds,omitempty"` } diff --git a/service/response/incident_response.go b/service/response/incident_response.go index dce46b1..d6f2642 100644 --- a/service/response/incident_response.go +++ b/service/response/incident_response.go @@ -3,37 +3,49 @@ package service import ( "github.com/lib/pq" "houston/model/incident" + "houston/model/product" + "houston/model/team" "time" ) type IncidentResponse struct { - ID uint `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Status uint `json:"status"` - StatusName string `json:"statusName"` - SeverityId uint `json:"severityId"` - SeverityName string `json:"severityName"` - IncidentName string `json:"incidentName"` - SlackChannel string `json:"slackChannel"` - DetectionTime *time.Time `json:"detectionTime"` - StartTime time.Time `json:"startTime"` - EndTime *time.Time `json:"endTime"` - TeamId uint `json:"teamId"` - TeamName string `json:"teamName"` - JiraLinks pq.StringArray `json:"jiraLinks"` - ConfluenceId *string `json:"confluenceId"` - SeverityTat time.Time `json:"severityTat"` - RemindMeAt *time.Time `json:"remindMeAt"` - EnableReminder bool `json:"enableReminder"` - CreatedBy string `json:"createdBy"` - UpdatedBy string `json:"updatedBy"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - RcaLink string `json:"rcaLink"` + ID uint `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Status uint `json:"status"` + StatusName string `json:"statusName"` + SeverityId uint `json:"severityId"` + SeverityName string `json:"severityName"` + IncidentName string `json:"incidentName"` + SlackChannel string `json:"slackChannel"` + DetectionTime *time.Time `json:"detectionTime"` + StartTime time.Time `json:"startTime"` + EndTime *time.Time `json:"endTime"` + TeamId uint `json:"teamId"` + TeamName string `json:"teamName"` + JiraLinks pq.StringArray `json:"jiraLinks"` + ConfluenceId *string `json:"confluenceId"` + SeverityTat time.Time `json:"severityTat"` + RemindMeAt *time.Time `json:"remindMeAt"` + EnableReminder bool `json:"enableReminder"` + CreatedBy string `json:"createdBy"` + UpdatedBy string `json:"updatedBy"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + RcaLink string `json:"rcaLink"` + ReportingTeam *IncidentHeaderOption `json:"reportingTeam"` + Products []IncidentHeaderOption `json:"products"` } func ConvertToIncidentResponse(incidentEntity incident.IncidentEntity) IncidentResponse { + var productResponse []IncidentHeaderOption + for _, productEntity := range incidentEntity.Products { + productResponse = append(productResponse, *ProductDTOToIncidentHeaderOption(productEntity.ToDTO())) + } + var reportingTeamResponse *IncidentHeaderOption = nil + if incidentEntity.ReportingTeamId != nil { + reportingTeamResponse = TeamDTOToIncidentHeaderOption(&team.TeamDTO{ID: *incidentEntity.ReportingTeamId}) + } return IncidentResponse{ ID: incidentEntity.ID, Title: incidentEntity.Title, @@ -55,5 +67,21 @@ func ConvertToIncidentResponse(incidentEntity incident.IncidentEntity) IncidentR UpdatedBy: incidentEntity.UpdatedBy, CreatedAt: incidentEntity.CreatedAt, UpdatedAt: incidentEntity.UpdatedAt, + ReportingTeam: reportingTeamResponse, + Products: productResponse, + } +} + +func TeamDTOToIncidentHeaderOption(t *team.TeamDTO) *IncidentHeaderOption { + return &IncidentHeaderOption{ + Value: t.ID, + Label: t.Name, + } +} + +func ProductDTOToIncidentHeaderOption(p *product.ProductDTO) *IncidentHeaderOption { + return &IncidentHeaderOption{ + Value: p.ProductID, + Label: p.ProductName, } } diff --git a/service/response/incidnt_orchestration_response.go b/service/response/incidnt_orchestration_response.go index eec2b1b..c155e49 100644 --- a/service/response/incidnt_orchestration_response.go +++ b/service/response/incidnt_orchestration_response.go @@ -5,17 +5,86 @@ import ( "houston/model/team" ) -type AssignerAndResponderTeams struct { - AssignerTeam *ProductAndUserTeams `json:"assignerTeam"` +type ReportingAndResponderTeams struct { + ReportingTeam *ProductAndUserTeams `json:"reportingTeam"` ResponderTeam *ProductAndUserTeams `json:"responderTeam"` } +func (a *ReportingAndResponderTeams) ToResponse() *ReportingAndResponderTeamsResponse { + return &ReportingAndResponderTeamsResponse{ + ReportingTeam: a.ReportingTeam.ToResponse(), + ResponderTeam: a.ResponderTeam.ToResponse(), + } +} + +type ReportingAndResponderTeamsResponse struct { + ReportingTeam *ProductAndUserTeamsResponse `json:"reportingTeam"` + ResponderTeam *ProductAndUserTeamsResponse `json:"responderTeam"` +} + type ProductsOfUser struct { - DefaultProduct product.ProductDTO `json:"defaultProduct"` + DefaultProduct *product.ProductDTO `json:"defaultProduct"` Products []product.ProductDTO `json:"products"` } +type ProductsForUpdate struct { + DefaultProducts []product.ProductDTO `json:"defaultProducts"` + Products []product.ProductDTO `json:"products"` +} + +type ProductsOfUserResponse struct { + DefaultProduct *IncidentHeaderOption `json:"defaultProduct"` + Products []IncidentHeaderOption `json:"products"` +} + +func (p *ProductsOfUser) ToResponse() *ProductsOfUserResponse { + var products []IncidentHeaderOption + for _, p := range p.Products { + products = append(products, IncidentHeaderOption{ + Value: p.ProductID, + Label: p.ProductName, + }) + } + var dp *IncidentHeaderOption = nil + if p.DefaultProduct != nil { + dp = &IncidentHeaderOption{ + Value: p.DefaultProduct.ProductID, + Label: p.DefaultProduct.ProductName, + } + } + return &ProductsOfUserResponse{ + DefaultProduct: dp, + Products: products, + } +} + type ProductAndUserTeams struct { - DefaultTeam team.TeamDTO `json:"defaultTeam"` + DefaultTeam *team.TeamDTO `json:"defaultTeam"` Teams []team.TeamDTO `json:"teams"` } + +func (p *ProductAndUserTeams) ToResponse() *ProductAndUserTeamsResponse { + var teams []IncidentHeaderOption + for _, t := range p.Teams { + teams = append(teams, IncidentHeaderOption{ + Value: t.ID, + Label: t.Name, + }) + } + var dt *IncidentHeaderOption + if p.DefaultTeam != nil { + dt = &IncidentHeaderOption{ + Value: p.DefaultTeam.ID, + Label: p.DefaultTeam.Name, + } + } + return &ProductAndUserTeamsResponse{ + DefaultTeam: dt, + Teams: teams, + } +} + +type ProductAndUserTeamsResponse struct { + DefaultTeam *IncidentHeaderOption `json:"defaultTeam"` + Teams []IncidentHeaderOption `json:"teams"` +} diff --git a/service/utils/validations.go b/service/utils/validations.go index 2e99e67..07bf2db 100644 --- a/service/utils/validations.go +++ b/service/utils/validations.go @@ -30,7 +30,6 @@ func ValidatePage(pageSize, pageNumber string) (int64, int64, error) { } func ValidateUpdateIncidentRequest(request service.UpdateIncidentRequest, userEmail string) error { - emptyMetaData := incident.CreateIncidentMetadata{} if userEmail == "" { return errors.New("user email header missing in update request") } @@ -38,15 +37,15 @@ func ValidateUpdateIncidentRequest(request service.UpdateIncidentRequest, userEm return errors.New("id should be present in update request") } if request.SeverityId == "" && request.Status == "" && request.TeamId == "" && - request.MetaData == emptyMetaData { + request.MetaData == nil && request.ProductIDs == nil { return errors.New("update request should contain at least one field to update") } if len(request.ProductIDs) > 0 && request.TeamId == "" { - return errors.New("assigner team ID must be provided when product is being updated") + return errors.New("responder team ID must be provided when product is being updated") } - if request.MetaData != emptyMetaData && (request.MetaData.CustomerId == uuid.Nil || request.MetaData.PhoneNumber == "" || request.MetaData.CrmTicketCreationTime == nil || request.MetaData.TicketId == "" || request.MetaData.TicketGroup == "" || request.MetaData.AgentName == "") { + if request.MetaData != nil && (request.MetaData.CustomerId == uuid.Nil || request.MetaData.PhoneNumber == "" || request.MetaData.CrmTicketCreationTime == nil || request.MetaData.TicketId == "" || request.MetaData.TicketGroup == "" || request.MetaData.AgentName == "") { return errors.New("metadata should contain customer id, phone number and crm ticket creation time") } if len(request.Justification) > 100 { @@ -127,8 +126,8 @@ func ValidateCreateIncidentRequestV3(request incident.CreateIncidentRequestV3) e if request.CreatedBy == "" { return errors.New("created by should be present") } - if request.AssignerTeamID == 0 { - return errors.New("assigner team ID should be present") + if request.ReportingTeamID == 0 { + return errors.New("reporting team team ID should be present") } if len(request.ProductIds) == 0 { return errors.New("product I.Ds should be present")