package services import ( "context" "database/sql" "errors" "github.com/rede5/gohorsejobs/backend/internal/models" ) type TicketService struct { DB *sql.DB } func NewTicketService(db *sql.DB) *TicketService { return &TicketService{DB: db} } func (s *TicketService) CreateTicket(ctx context.Context, userID string, subject, priority string) (*models.Ticket, error) { if priority == "" { priority = "medium" } query := ` INSERT INTO tickets (user_id, subject, status, priority, created_at, updated_at) VALUES ($1, $2, 'open', $3, NOW(), NOW()) RETURNING id, user_id, subject, status, priority, created_at, updated_at ` var t models.Ticket err := s.DB.QueryRowContext(ctx, query, userID, subject, priority).Scan( &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ) if err != nil { return nil, err } return &t, nil } func (s *TicketService) ListTickets(ctx context.Context, userID string) ([]models.Ticket, error) { query := ` SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets WHERE user_id = $1 ORDER BY updated_at DESC ` rows, err := s.DB.QueryContext(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() tickets := []models.Ticket{} for rows.Next() { var t models.Ticket if err := rows.Scan( &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ); err != nil { return nil, err } tickets = append(tickets, t) } return tickets, nil } func (s *TicketService) GetTicket(ctx context.Context, ticketID string, userID string, isAdmin bool) (*models.Ticket, []models.TicketMessage, error) { // 1. Get Ticket queryTicket := ` SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets WHERE id = $1 ` args := []any{ticketID} if !isAdmin { queryTicket += " AND user_id = $2" args = append(args, userID) } var t models.Ticket err := s.DB.QueryRowContext(ctx, queryTicket, args...).Scan( &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil, errors.New("ticket not found") } return nil, nil, err } // 2. Get Messages queryMsgs := ` SELECT id, ticket_id, user_id, message, created_at FROM ticket_messages WHERE ticket_id = $1 ORDER BY created_at ASC ` rows, err := s.DB.QueryContext(ctx, queryMsgs, ticketID) if err != nil { return nil, nil, err } defer rows.Close() messages := []models.TicketMessage{} for rows.Next() { var m models.TicketMessage if err := rows.Scan( &m.ID, &m.TicketID, &m.UserID, &m.Message, &m.CreatedAt, ); err != nil { return nil, nil, err } messages = append(messages, m) } return &t, messages, nil } func (s *TicketService) AddMessage(ctx context.Context, ticketID string, userID string, message string, isAdmin bool) (*models.TicketMessage, error) { // Verify ticket ownership first (or admin access) var count int query := "SELECT COUNT(*) FROM tickets WHERE id = $1 AND user_id = $2" args := []any{ticketID, userID} if isAdmin { query = "SELECT COUNT(*) FROM tickets WHERE id = $1" args = []any{ticketID} } err := s.DB.QueryRowContext(ctx, query, args...).Scan(&count) if err != nil { return nil, err } if count == 0 { return nil, errors.New("ticket not found") } query = ` INSERT INTO ticket_messages (ticket_id, user_id, message, created_at) VALUES ($1, $2, $3, NOW()) RETURNING id, ticket_id, user_id, message, created_at ` var m models.TicketMessage err = s.DB.QueryRowContext(ctx, query, ticketID, userID, message).Scan( &m.ID, &m.TicketID, &m.UserID, &m.Message, &m.CreatedAt, ) if err != nil { return nil, err } // Update ticket updated_at _, _ = s.DB.ExecContext(ctx, "UPDATE tickets SET updated_at = NOW() WHERE id = $1", ticketID) return &m, nil } func (s *TicketService) UpdateTicket(ctx context.Context, ticketID string, userID string, status *string, priority *string, isAdmin bool) (*models.Ticket, error) { // Verify ownership (or admin access) var ownerID string err := s.DB.QueryRowContext(ctx, "SELECT user_id FROM tickets WHERE id = $1", ticketID).Scan(&ownerID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, errors.New("ticket not found") } return nil, err } // Only owner or admin can update if ownerID != userID && !isAdmin { return nil, errors.New("unauthorized") } // Build dynamic update setClauses := []string{"updated_at = NOW()"} args := []interface{}{} argIdx := 1 if status != nil { setClauses = append(setClauses, "status = $"+string(rune('0'+argIdx))) args = append(args, *status) argIdx++ } if priority != nil { setClauses = append(setClauses, "priority = $"+string(rune('0'+argIdx))) args = append(args, *priority) argIdx++ } args = append(args, ticketID) query := "UPDATE tickets SET " + joinStrings(setClauses, ", ") + " WHERE id = $" + string(rune('0'+argIdx)) + " RETURNING id, user_id, subject, status, priority, created_at, updated_at" var t models.Ticket err = s.DB.QueryRowContext(ctx, query, args...).Scan( &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ) if err != nil { return nil, err } return &t, nil } // CloseTicket is a convenience method to close a ticket func (s *TicketService) CloseTicket(ctx context.Context, ticketID string, userID string, isAdmin bool) (*models.Ticket, error) { status := "closed" return s.UpdateTicket(ctx, ticketID, userID, &status, nil, isAdmin) } // DeleteTicket removes a ticket (admin only) func (s *TicketService) DeleteTicket(ctx context.Context, ticketID string) error { // First delete messages _, err := s.DB.ExecContext(ctx, "DELETE FROM ticket_messages WHERE ticket_id = $1", ticketID) if err != nil { return err } // Then delete ticket result, err := s.DB.ExecContext(ctx, "DELETE FROM tickets WHERE id = $1", ticketID) if err != nil { return err } rowsAffected, _ := result.RowsAffected() if rowsAffected == 0 { return errors.New("ticket not found") } return nil } // ListAllTickets returns all tickets (for admin) func (s *TicketService) ListAllTickets(ctx context.Context, status string) ([]models.Ticket, error) { query := ` SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets ` args := []interface{}{} if status != "" { query += " WHERE status = $1" args = append(args, status) } query += " ORDER BY updated_at DESC" rows, err := s.DB.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() tickets := []models.Ticket{} for rows.Next() { var t models.Ticket if err := rows.Scan( &t.ID, &t.UserID, &t.Subject, &t.Status, &t.Priority, &t.CreatedAt, &t.UpdatedAt, ); err != nil { return nil, err } tickets = append(tickets, t) } return tickets, nil } // Helper function func joinStrings(strs []string, sep string) string { if len(strs) == 0 { return "" } result := strs[0] for i := 1; i < len(strs); i++ { result += sep + strs[i] } return result }