From da4a62678d40e27246c57c727b4523e651dc6a6d Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Sat, 20 Jun 2026 18:03:17 +0530 Subject: [PATCH] add notification trigger on ticket assignment - add Assignee field to Tickets struct - send notification via v2 bot when ticket is assigned to a user - notification only triggers when assignee changes (avoids duplicate notifications) - add unit tests for ticket assignment notification Signed-off-by: Gautam Kumar --- db/structs.go | 1 + handlers/ticket.go | 17 ++++++ handlers/ticket_test.go | 124 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) diff --git a/db/structs.go b/db/structs.go index 1c7c9c8a3..e9d1e3941 100644 --- a/db/structs.go +++ b/db/structs.go @@ -1090,6 +1090,7 @@ type Tickets struct { Dependency []int `gorm:"type:integer[]" json:"dependency"` Description string `gorm:"type:text" json:"description"` Status TicketStatus `gorm:"type:varchar(50);default:'DRAFT'" json:"status"` + Assignee string `gorm:"type:varchar(255);default:''" json:"assignee"` Version int `gorm:"type:integer;default:0" json:"version"` Author *Author `gorm:"type:varchar(50)" json:"author,omitempty"` AuthorID *string `gorm:"type:varchar(255)" json:"author_id,omitempty"` diff --git a/handlers/ticket.go b/handlers/ticket.go index 3668e75df..b02ce8735 100644 --- a/handlers/ticket.go +++ b/handlers/ticket.go @@ -197,6 +197,7 @@ func (th *ticketHandler) UpdateTicket(w http.ResponseWriter, r *http.Request) { Status: updateRequest.Ticket.Status, Amount: updateRequest.Ticket.Amount, Category: updateRequest.Ticket.Category, + Assignee: updateRequest.Ticket.Assignee, Version: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), @@ -215,6 +216,7 @@ func (th *ticketHandler) UpdateTicket(w http.ResponseWriter, r *http.Request) { Status: updateRequest.Ticket.Status, Amount: updateRequest.Ticket.Amount, Category: updateRequest.Ticket.Category, + Assignee: updateRequest.Ticket.Assignee, Version: existingTicket.Version + 1, Author: updateRequest.Ticket.Author, AuthorID: updateRequest.Ticket.AuthorID, @@ -235,6 +237,21 @@ func (th *ticketHandler) UpdateTicket(w http.ResponseWriter, r *http.Request) { return } + if newTicket.Assignee != "" { + var previousAssignee string + if existingTicket.UUID != uuid.Nil { + previousAssignee = existingTicket.Assignee + } + + if newTicket.Assignee != previousAssignee { + person := th.db.GetPersonByPubkey(newTicket.Assignee) + if person.OwnerPubKey != "" { + msg := fmt.Sprintf("You have been assigned a new ticket: %s. %s/bounty/%s", newTicket.Name, os.Getenv("HOST"), newTicket.UUID.String()) + processNotification(newTicket.Assignee, "ticket_assigned", msg, person.OwnerAlias, person.OwnerRouteHint) + } + } + } + if updateRequest.Metadata.Source == "websocket" && updateRequest.Metadata.ID != "" { ticketMsg := websocket.TicketMessage{ BroadcastType: "direct", diff --git a/handlers/ticket_test.go b/handlers/ticket_test.go index 5b72135b5..e1c719d36 100644 --- a/handlers/ticket_test.go +++ b/handlers/ticket_test.go @@ -1840,3 +1840,127 @@ func TestTicketsToBounties(t *testing.T) { }) } } + +func TestUpdateTicketAssignmentNotification(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + tHandler := NewTicketHandler(&http.Client{}, db.TestDB) + + person := db.Person{ + Uuid: uuid.New().String(), + OwnerAlias: "assignee-alias", + UniqueName: "assignee-unique", + OwnerPubKey: "assignee-pubkey", + PriceToMeet: 0, + Description: "assignee description", + OwnerRouteHint: "route_hint", + } + db.TestDB.CreateOrEditPerson(person) + + workspace := db.Workspace{ + Uuid: uuid.New().String(), + Name: "test-workspace-" + uuid.New().String(), + OwnerPubKey: "owner-pubkey", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + feature := db.WorkspaceFeatures{ + Uuid: uuid.New().String(), + WorkspaceUuid: workspace.Uuid, + Name: "test-feature", + } + db.TestDB.CreateOrEditFeature(feature) + + featurePhase := db.FeaturePhase{ + Uuid: uuid.New().String(), + FeatureUuid: feature.Uuid, + WorkspaceId: workspace.Uuid, + Name: "test-phase", + } + db.TestDB.CreateOrEditFeaturePhase(featurePhase) + + t.Run("should trigger notification when ticket is assigned", func(t *testing.T) { + ticketUUID := uuid.New() + ticket := &db.Tickets{ + UUID: ticketUUID, + FeatureUUID: feature.Uuid, + PhaseUUID: featurePhase.Uuid, + Name: "test ticket", + Status: db.DraftTicket, + Assignee: "", + } + db.TestDB.CreateOrEditTicket(ticket) + + updateRequest := UpdateTicketRequest{ + Ticket: &db.Tickets{ + UUID: ticketUUID, + FeatureUUID: feature.Uuid, + PhaseUUID: featurePhase.Uuid, + Name: "test ticket", + Status: db.ActiveTicket, + Assignee: "assignee-pubkey", + }, + } + bodyBytes, _ := json.Marshal(updateRequest) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/bounties/ticket/"+ticketUUID.String(), bytes.NewReader(bodyBytes)) + req = req.WithContext(context.WithValue(req.Context(), auth.ContextKey, "owner-pubkey")) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", ticketUUID.String()) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + tHandler.UpdateTicket(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response db.Tickets + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "assignee-pubkey", response.Assignee) + }) + + t.Run("should not trigger notification when assignee is unchanged", func(t *testing.T) { + ticketUUID := uuid.New() + ticket := &db.Tickets{ + UUID: ticketUUID, + FeatureUUID: feature.Uuid, + PhaseUUID: featurePhase.Uuid, + Name: "test ticket unchanged", + Status: db.DraftTicket, + Assignee: "assignee-pubkey", + } + db.TestDB.CreateOrEditTicket(ticket) + + updateRequest := UpdateTicketRequest{ + Ticket: &db.Tickets{ + UUID: ticketUUID, + FeatureUUID: feature.Uuid, + PhaseUUID: featurePhase.Uuid, + Name: "test ticket unchanged", + Status: db.ActiveTicket, + Assignee: "assignee-pubkey", + }, + } + bodyBytes, _ := json.Marshal(updateRequest) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/bounties/ticket/"+ticketUUID.String(), bytes.NewReader(bodyBytes)) + req = req.WithContext(context.WithValue(req.Context(), auth.ContextKey, "owner-pubkey")) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", ticketUUID.String()) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + tHandler.UpdateTicket(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var response db.Tickets + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, "assignee-pubkey", response.Assignee) + }) +}