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 } 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) VALUES(?,?,?,?,?,?,?)`, n, t.UserID, t.Panel, t.Type, t.ChannelID, t.OpenedAt.UTC(), "open", ) 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) VALUES(?,?,?,?,?,?,?)`, t.TicketNumber, t.UserID, t.Panel, t.Type, t.ChannelID, t.OpenedAt.UTC(), "open", ) 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 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 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 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 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() } 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) 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); err != nil { return nil, err } result = append(result, &t) } return result, rows.Err() }