Files
bot_ticket_r6_elite/internal/db/tickets.go
T
2026-05-02 03:23:51 +02:00

204 lines
6.6 KiB
Go

package db
import (
"context"
"database/sql"
"fmt"
"time"
)
type Ticket struct {
ID int64
TicketNumber int
UserID string
Panel string
Type string
ChannelID string
OpenedAt time.Time
ClaimedBy sql.NullString
ClaimedAt sql.NullTime
ClosedAt sql.NullTime
ClosedBy sql.NullString
Reason sql.NullString
TranscriptPath sql.NullString
Status string
TicketTitle sql.NullString
TicketDescription sql.NullString
}
type TicketRepo struct{ db *sql.DB }
func NewTicketRepo(db *sql.DB) *TicketRepo { return &TicketRepo{db: db} }
// nextNumber returns the next ticket_number for the given type within a transaction.
func (r *TicketRepo) nextNumber(ctx context.Context, tx *sql.Tx, ticketType string) (int, error) {
var n int
err := tx.QueryRowContext(ctx,
`SELECT COALESCE(MAX(ticket_number),0)+1 FROM tickets WHERE type=?`, ticketType,
).Scan(&n)
return n, err
}
func (r *TicketRepo) Insert(ctx context.Context, t *Ticket) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() //nolint
n, err := r.nextNumber(ctx, tx, t.Type)
if err != nil {
return err
}
t.TicketNumber = n
res, err := tx.ExecContext(ctx, `
INSERT INTO tickets(ticket_number,user_id,panel,type,channel_id,opened_at,status,ticket_title,ticket_description)
VALUES(?,?,?,?,?,?,?,?,?)`,
n, t.UserID, t.Panel, t.Type, t.ChannelID, t.OpenedAt.UTC(), "open",
t.TicketTitle, t.TicketDescription,
)
if err != nil {
return fmt.Errorf("insert ticket: %w", err)
}
t.ID, _ = res.LastInsertId()
t.Status = "open"
return tx.Commit()
}
// InsertWithNumber inserts a ticket where TicketNumber is already set (e.g. convocations).
func (r *TicketRepo) InsertWithNumber(ctx context.Context, t *Ticket) error {
res, err := r.db.ExecContext(ctx, `
INSERT INTO tickets(ticket_number,user_id,panel,type,channel_id,opened_at,status,ticket_title,ticket_description)
VALUES(?,?,?,?,?,?,?,?,?)`,
t.TicketNumber, t.UserID, t.Panel, t.Type, t.ChannelID, t.OpenedAt.UTC(), "open",
t.TicketTitle, t.TicketDescription,
)
if err != nil {
return fmt.Errorf("insert ticket with number: %w", err)
}
t.ID, _ = res.LastInsertId()
t.Status = "open"
return nil
}
func (r *TicketRepo) GetByChannelID(ctx context.Context, channelID string) (*Ticket, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id,ticket_number,user_id,panel,type,channel_id,opened_at,
claimed_by,claimed_at,closed_at,closed_by,reason,transcript_path,status,ticket_title,ticket_description
FROM tickets WHERE channel_id=?`, channelID)
return scanTicket(row)
}
func (r *TicketRepo) GetByID(ctx context.Context, id int64) (*Ticket, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id,ticket_number,user_id,panel,type,channel_id,opened_at,
claimed_by,claimed_at,closed_at,closed_by,reason,transcript_path,status,ticket_title,ticket_description
FROM tickets WHERE id=?`, id)
return scanTicket(row)
}
func (r *TicketRepo) HasOpenTicket(ctx context.Context, userID, ticketType string) (*Ticket, error) {
row := r.db.QueryRowContext(ctx, `
SELECT id,ticket_number,user_id,panel,type,channel_id,opened_at,
claimed_by,claimed_at,closed_at,closed_by,reason,transcript_path,status,ticket_title,ticket_description
FROM tickets WHERE user_id=? AND type=? AND status IN('open','claimed')
LIMIT 1`, userID, ticketType)
t, err := scanTicket(row)
if err == sql.ErrNoRows {
return nil, nil
}
return t, err
}
func (r *TicketRepo) SetClaimed(ctx context.Context, id int64, staffID string, at time.Time) error {
_, err := r.db.ExecContext(ctx,
`UPDATE tickets SET status='claimed', claimed_by=?, claimed_at=? WHERE id=?`,
staffID, at.UTC(), id)
return err
}
func (r *TicketRepo) SetClosed(ctx context.Context, id int64, closedBy, reason, transcriptPath string, at time.Time) error {
_, err := r.db.ExecContext(ctx, `
UPDATE tickets SET status='closed', closed_at=?, closed_by=?, reason=?, transcript_path=?
WHERE id=?`,
at.UTC(), closedBy, reason, transcriptPath, id)
return err
}
func (r *TicketRepo) SetClosedByChannel(ctx context.Context, channelID, reason string) error {
_, err := r.db.ExecContext(ctx, `
UPDATE tickets SET status='closed', closed_at=?, reason=?
WHERE channel_id=? AND status IN('open','claimed')`,
time.Now().UTC(), reason, channelID)
return err
}
func (r *TicketRepo) ListOpen(ctx context.Context) ([]*Ticket, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT id,ticket_number,user_id,panel,type,channel_id,opened_at,
claimed_by,claimed_at,closed_at,closed_by,reason,transcript_path,status,ticket_title,ticket_description
FROM tickets WHERE status='open'`)
if err != nil {
return nil, err
}
defer rows.Close()
return scanTickets(rows)
}
func (r *TicketRepo) UpdateChannelID(ctx context.Context, id int64, newChannelID string) error {
_, err := r.db.ExecContext(ctx, `UPDATE tickets SET channel_id=? WHERE id=?`, newChannelID, id)
return err
}
func (r *TicketRepo) NextConvocationNumber(ctx context.Context) (int, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return 0, err
}
defer tx.Rollback() //nolint
var n int
if err := tx.QueryRowContext(ctx, `UPDATE convocation_counter SET count=count+1 WHERE id=1 RETURNING count`).Scan(&n); err != nil {
return 0, err
}
return n, tx.Commit()
}
// NullStr returns a valid NullString for non-empty strings, invalid (NULL) for empty.
func NullStr(s string) sql.NullString {
return sql.NullString{String: s, Valid: s != ""}
}
// SetTitleDescription updates the title and description of a ticket after modal submission.
func (r *TicketRepo) SetTitleDescription(ctx context.Context, id int64, title, description string) error {
_, err := r.db.ExecContext(ctx,
`UPDATE tickets SET ticket_title=?, ticket_description=? WHERE id=?`,
title, description, id)
return err
}
func scanTicket(row *sql.Row) (*Ticket, error) {
var t Ticket
err := row.Scan(&t.ID, &t.TicketNumber, &t.UserID, &t.Panel, &t.Type, &t.ChannelID,
&t.OpenedAt, &t.ClaimedBy, &t.ClaimedAt, &t.ClosedAt, &t.ClosedBy,
&t.Reason, &t.TranscriptPath, &t.Status, &t.TicketTitle, &t.TicketDescription)
if err != nil {
return nil, err
}
return &t, nil
}
func scanTickets(rows *sql.Rows) ([]*Ticket, error) {
var result []*Ticket
for rows.Next() {
var t Ticket
if err := rows.Scan(&t.ID, &t.TicketNumber, &t.UserID, &t.Panel, &t.Type, &t.ChannelID,
&t.OpenedAt, &t.ClaimedBy, &t.ClaimedAt, &t.ClosedAt, &t.ClosedBy,
&t.Reason, &t.TranscriptPath, &t.Status, &t.TicketTitle, &t.TicketDescription); err != nil {
return nil, err
}
result = append(result, &t)
}
return result, rows.Err()
}