123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- package handlers
- import (
- "fmt"
- "net/http"
- "strconv"
- "time"
- "git.linuxforward.com/byop/byop-engine/dbstore"
- "git.linuxforward.com/byop/byop-engine/models"
- "github.com/gin-gonic/gin"
- "github.com/go-playground/validator/v10"
- )
- // TicketHandler handles ticket-related operations
- type TicketHandler struct {
- Store dbstore.Store // Use the defined interface
- Validate *validator.Validate // For request validation
- }
- // NewTicketHandler creates a new TicketHandler
- func NewTicketHandler(store dbstore.Store) *TicketHandler {
- return &TicketHandler{
- Store: store,
- Validate: validator.New(),
- }
- }
- // RegisterRoutes registers routes for ticket operations
- func (h *TicketHandler) RegisterRoutes(r *gin.RouterGroup) {
- r.GET("/", h.ListTickets)
- r.POST("/", h.CreateTicket)
- r.GET("/:id", h.GetTicket)
- r.PUT("/:id", h.UpdateTicket)
- r.GET("/:id/comments", h.GetTicketComments)
- r.POST("/:id/comments", h.AddTicketComment)
- r.POST("/:id/resolve", h.ResolveTicket)
- }
- // ListTickets returns all tickets
- func (h *TicketHandler) ListTickets(c *gin.Context) {
- ctx := c.Request.Context()
- tickets, err := h.Store.GetTickets(ctx)
- if err != nil {
- appErr := models.NewErrInternalServer("list_tickets_failed", fmt.Errorf("Failed to list tickets: %w", err))
- models.RespondWithError(c, appErr)
- return
- }
- if tickets == nil {
- tickets = []models.Ticket{} // Return empty slice instead of null
- }
- c.JSON(http.StatusOK, tickets)
- }
- // CreateTicketInput defines the input for creating a ticket
- type CreateTicketInput struct {
- Title string `json:"title" validate:"required,min=3,max=255"`
- Description string `json:"description" validate:"required,min=10"`
- ClientID int `json:"client_id" validate:"required,gt=0"` // Assuming ClientID is mandatory for a ticket
- UserID *int `json:"user_id,omitempty" validate:"omitempty,gt=0"`
- Priority string `json:"priority" validate:"omitempty,oneof=low medium high critical"`
- }
- // CreateTicket creates a new ticket
- func (h *TicketHandler) CreateTicket(c *gin.Context) {
- ctx := c.Request.Context()
- var input CreateTicketInput
- if err := c.ShouldBindJSON(&input); err != nil {
- appErr := models.NewErrValidation("invalid_ticket_input", map[string]string{"body": "Invalid request body"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- if err := h.Validate.StructCtx(ctx, input); err != nil {
- errors := models.ExtractValidationErrors(err)
- appErr := models.NewErrValidation("ticket_validation_failed", errors, err)
- models.RespondWithError(c, appErr)
- return
- }
- // authUserID, _ := ctx.Value("userID").(int) // Example: Get authenticated user ID
- ticket := models.Ticket{
- Title: input.Title,
- Description: input.Description,
- ClientID: input.ClientID,
- UserID: input.UserID,
- Status: models.TicketStatusOpen,
- Priority: input.Priority,
- // CreatedAt and UpdatedAt will be set by the store or DB
- }
- if ticket.Priority == "" {
- ticket.Priority = models.TicketPriorityMedium // Default priority
- }
- if err := h.Store.CreateTicket(ctx, &ticket); err != nil {
- appErr := models.NewErrInternalServer("create_ticket_failed", fmt.Errorf("Failed to create ticket: %w", err))
- models.RespondWithError(c, appErr)
- return
- }
- c.JSON(http.StatusCreated, ticket)
- }
- // GetTicket returns a specific ticket
- func (h *TicketHandler) GetTicket(c *gin.Context) {
- ctx := c.Request.Context()
- idStr := c.Param("id")
- id, err := strconv.ParseInt(idStr, 10, 64)
- if err != nil {
- appErr := models.NewErrValidation("invalid_ticket_id_format", map[string]string{"id": "Invalid ticket ID format"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- ticket, err := h.Store.GetTicketByID(ctx, int(id))
- if err != nil {
- if models.IsErrNotFound(err) {
- appErr := models.NewErrNotFound("ticket_not_found", fmt.Errorf("Ticket with ID %d not found: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- appErr := models.NewErrInternalServer("get_ticket_failed", fmt.Errorf("Failed to get ticket %d: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- c.JSON(http.StatusOK, ticket)
- }
- // UpdateTicketInput defines the input for updating a ticket
- type UpdateTicketInput struct {
- Title *string `json:"title,omitempty" validate:"omitempty,min=3,max=255"`
- Description *string `json:"description,omitempty" validate:"omitempty,min=10"`
- Priority *string `json:"priority,omitempty" validate:"omitempty,oneof=low medium high critical"`
- Status *string `json:"status,omitempty" validate:"omitempty,oneof=open in_progress resolved closed"`
- AssignedTo *int `json:"assigned_to,omitempty" validate:"omitempty,gt=0"`
- }
- // UpdateTicket updates a ticket
- func (h *TicketHandler) UpdateTicket(c *gin.Context) {
- ctx := c.Request.Context()
- idStr := c.Param("id")
- id, err := strconv.ParseInt(idStr, 10, 64)
- if err != nil {
- appErr := models.NewErrValidation("invalid_ticket_id_format", map[string]string{"id": "Invalid ticket ID format"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- var input UpdateTicketInput
- if err := c.ShouldBindJSON(&input); err != nil {
- appErr := models.NewErrValidation("invalid_update_ticket_input", map[string]string{"body": "Invalid request body"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- if err := h.Validate.StructCtx(ctx, input); err != nil {
- errors := models.ExtractValidationErrors(err)
- appErr := models.NewErrValidation("update_ticket_validation_failed", errors, err)
- models.RespondWithError(c, appErr)
- return
- }
- ticket, err := h.Store.GetTicketByID(ctx, int(id))
- if err != nil {
- if models.IsErrNotFound(err) {
- appErr := models.NewErrNotFound("ticket_not_found_for_update", fmt.Errorf("Ticket with ID %d not found for update: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- appErr := models.NewErrInternalServer("get_ticket_for_update_failed", fmt.Errorf("Failed to get ticket %d for update: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- // Apply updates
- updated := false
- if input.Title != nil {
- ticket.Title = *input.Title
- updated = true
- }
- if input.Description != nil {
- ticket.Description = *input.Description
- updated = true
- }
- if input.Priority != nil {
- ticket.Priority = *input.Priority
- updated = true
- }
- if input.Status != nil {
- ticket.Status = *input.Status
- updated = true
- }
- if input.AssignedTo != nil {
- ticket.AssignedTo = input.AssignedTo
- updated = true
- }
- if !updated {
- c.JSON(http.StatusOK, ticket) // No changes, return current ticket
- return
- }
- if err := h.Store.UpdateTicket(ctx, ticket); err != nil {
- appErr := models.NewErrInternalServer("update_ticket_failed", fmt.Errorf("Failed to update ticket %d: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- c.JSON(http.StatusOK, ticket)
- }
- // GetTicketComments returns comments for a ticket
- func (h *TicketHandler) GetTicketComments(c *gin.Context) {
- ctx := c.Request.Context()
- idStr := c.Param("id")
- ticketID, err := strconv.ParseInt(idStr, 10, 64)
- if err != nil {
- appErr := models.NewErrValidation("invalid_ticket_id_for_comments", map[string]string{"id": "Invalid ticket ID format for comments"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- comments, err := h.Store.GetTicketComments(ctx, int(ticketID))
- if err != nil {
- // If the error indicates the ticket itself was not found, that's a 404 for the ticket.
- // Otherwise, it's an internal error fetching comments.
- // Assuming GetTicketComments might return ErrNotFound if the ticket doesn't exist.
- if models.IsErrNotFound(err) { // This could be ambiguous: ticket not found OR no comments found and store treats it as not found.
- // To be more precise, one might first check if ticket exists, then fetch comments.
- // For now, assume this means ticket itself is not found.
- appErr := models.NewErrNotFound("ticket_not_found_for_comments", fmt.Errorf("Ticket with ID %d not found when fetching comments: %w", ticketID, err))
- models.RespondWithError(c, appErr)
- return
- }
- appErr := models.NewErrInternalServer("get_ticket_comments_failed", fmt.Errorf("Failed to get comments for ticket %d: %w", ticketID, err))
- models.RespondWithError(c, appErr)
- return
- }
- if comments == nil {
- comments = []models.TicketComment{} // Return empty slice
- }
- c.JSON(http.StatusOK, comments)
- }
- // AddTicketCommentInput defines the input for adding a comment
- type AddTicketCommentInput struct {
- Content string `json:"content" validate:"required,min=1"`
- // UserID will be taken from authenticated user context
- }
- // AddTicketComment adds a comment to a ticket
- func (h *TicketHandler) AddTicketComment(c *gin.Context) {
- ctx := c.Request.Context()
- idStr := c.Param("id")
- ticketID, err := strconv.ParseInt(idStr, 10, 64)
- if err != nil {
- appErr := models.NewErrValidation("invalid_ticket_id_for_add_comment", map[string]string{"id": "Invalid ticket ID format for adding comment"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- var input AddTicketCommentInput
- if err := c.ShouldBindJSON(&input); err != nil {
- appErr := models.NewErrValidation("invalid_comment_input", map[string]string{"body": "Invalid request body for comment"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- if err := h.Validate.StructCtx(ctx, input); err != nil {
- errors := models.ExtractValidationErrors(err)
- appErr := models.NewErrValidation("comment_validation_failed", errors, err)
- models.RespondWithError(c, appErr)
- return
- }
- // Get authenticated user ID (placeholder - replace with actual auth logic)
- authUserID := 1 // Example: Assume user ID 1 is authenticated
- // if !ok || authUserID == 0 {
- // appErr := models.NewErrUnauthorized("user_not_authenticated_for_comment", fmt.Errorf("User must be authenticated to comment"))
- // models.RespondWithError(c, appErr)
- // return
- // }
- comment := models.TicketComment{
- TicketID: int(ticketID),
- UserID: authUserID, // Set from authenticated user
- Content: input.Content,
- }
- if err := h.Store.CreateTicketComment(ctx, &comment); err != nil {
- // Check if the error is because the ticket doesn't exist (e.g., foreign key violation)
- if models.IsErrForeignKeyViolation(err) || models.IsErrNotFound(err) { // IsErrNotFound might be returned by store if ticket check fails
- appErr := models.NewErrNotFound("ticket_not_found_for_new_comment", fmt.Errorf("Ticket with ID %d not found, cannot add comment: %w", ticketID, err))
- models.RespondWithError(c, appErr)
- return
- }
- appErr := models.NewErrInternalServer("add_comment_failed", fmt.Errorf("Failed to add comment to ticket %d: %w", ticketID, err))
- models.RespondWithError(c, appErr)
- return
- }
- c.JSON(http.StatusCreated, comment)
- }
- // ResolveTicket resolves a ticket
- func (h *TicketHandler) ResolveTicket(c *gin.Context) {
- ctx := c.Request.Context()
- idStr := c.Param("id")
- id, err := strconv.ParseInt(idStr, 10, 64)
- if err != nil {
- appErr := models.NewErrValidation("invalid_ticket_id_for_resolve", map[string]string{"id": "Invalid ticket ID format for resolving"}, err)
- models.RespondWithError(c, appErr)
- return
- }
- ticket, err := h.Store.GetTicketByID(ctx, int(id))
- if err != nil {
- if models.IsErrNotFound(err) {
- appErr := models.NewErrNotFound("ticket_not_found_for_resolve", fmt.Errorf("Ticket with ID %d not found for resolve: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- appErr := models.NewErrInternalServer("get_ticket_for_resolve_failed", fmt.Errorf("Failed to get ticket %d for resolve: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- if ticket.Status == models.TicketStatusResolved || ticket.Status == models.TicketStatusClosed {
- appErr := models.NewErrValidation("ticket_already_resolved_or_closed", map[string]string{"status": fmt.Sprintf("Ticket is already %s", ticket.Status)}, nil)
- models.RespondWithError(c, appErr)
- return
- }
- ticket.Status = models.TicketStatusResolved
- now := time.Now()
- ticket.ResolvedAt = &now
- if err := h.Store.UpdateTicket(ctx, ticket); err != nil {
- appErr := models.NewErrInternalServer("resolve_ticket_failed", fmt.Errorf("Failed to resolve ticket %d: %w", id, err))
- models.RespondWithError(c, appErr)
- return
- }
- c.JSON(http.StatusOK, ticket)
- }
|