271 lines
7.2 KiB
Go
271 lines
7.2 KiB
Go
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, category, priority string) (*models.Ticket, error) {
|
|
if priority == "" {
|
|
priority = "medium"
|
|
}
|
|
if category == "" {
|
|
category = "other"
|
|
}
|
|
query := `
|
|
INSERT INTO tickets (user_id, subject, category, status, priority, created_at, updated_at)
|
|
VALUES ($1, $2, $3, 'open', $4, NOW(), NOW())
|
|
RETURNING id, user_id, subject, category, status, priority, created_at, updated_at
|
|
`
|
|
var t models.Ticket
|
|
err := s.DB.QueryRowContext(ctx, query, userID, subject, category, priority).Scan(
|
|
&t.ID, &t.UserID, &t.Subject, &t.Category, &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, category, 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.Category, &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, category, 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.Category, &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, category, 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.Category, &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, category, 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.Category, &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
|
|
}
|