package handlers import ( "fmt" "net/http" "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 uint `json:"client_id" validate:"required,gt=0"` // Assuming ClientID is mandatory for a ticket UserID *uint `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() id, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } ticket, err := h.Store.GetTicketByID(ctx, 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 *uint `json:"assigned_to,omitempty" validate:"omitempty,gt=0"` } // UpdateTicket updates a ticket func (h *TicketHandler) UpdateTicket(c *gin.Context) { ctx := c.Request.Context() id, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) 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, 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() ticketID, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } comments, err := h.Store.GetTicketComments(ctx, 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() ticketID, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) 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 := uint(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: 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() id, err := parseUintID(c, "id") if err != nil { models.RespondWithError(c, err) return } ticket, err := h.Store.GetTicketByID(ctx, 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) }