package tickets import ( "context" "database/sql" "reanimator/internal/domain" "reanimator/internal/idgen" ) type TicketRepository struct { db *sql.DB idgen *idgen.Generator } func NewTicketRepository(db *sql.DB) *TicketRepository { return &TicketRepository{ db: db, idgen: idgen.NewGenerator(db), } } func (r *TicketRepository) BeginTx(ctx context.Context) (*sql.Tx, error) { return r.db.BeginTx(ctx, nil) } type execer interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) } func execerFor(db *sql.DB, tx *sql.Tx) execer { if tx != nil { return tx } return db } func (r *TicketRepository) Upsert(ctx context.Context, tx *sql.Tx, ticket domain.Ticket) (string, error) { execer := execerFor(r.db, tx) // Generate ID for new tickets id, err := r.idgen.Generate(ctx, idgen.Ticket) if err != nil { return "", err } _, err = execer.ExecContext(ctx, `INSERT INTO tickets (id, source, external_id, title, status, opened_at, closed_at, url) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title = VALUES(title), status = VALUES(status), opened_at = VALUES(opened_at), closed_at = VALUES(closed_at), url = VALUES(url)`, id, ticket.Source, ticket.ExternalID, ticket.Title, ticket.Status, ticket.OpenedAt, ticket.ClosedAt, ticket.URL, ) if err != nil { return "", err } var resultID string row := execer.QueryRowContext(ctx, `SELECT id FROM tickets WHERE source = ? AND external_id = ?`, ticket.Source, ticket.ExternalID, ) if err := row.Scan(&resultID); err != nil { return "", err } return resultID, nil } func (r *TicketRepository) LinkToAsset(ctx context.Context, tx *sql.Tx, ticketID, assetID string) error { execer := execerFor(r.db, tx) // Generate ID for new ticket link id, err := r.idgen.Generate(ctx, idgen.TicketLink) if err != nil { return err } _, err = execer.ExecContext(ctx, `INSERT INTO ticket_links (id, ticket_id, machine_id) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE ticket_id = VALUES(ticket_id)`, id, ticketID, assetID, ) return err } func (r *TicketRepository) ListByAsset(ctx context.Context, assetID string) ([]domain.Ticket, error) { rows, err := r.db.QueryContext(ctx, `SELECT t.id, t.source, t.external_id, t.title, t.status, t.opened_at, t.closed_at, t.url, t.created_at, t.updated_at FROM ticket_links tl JOIN tickets t ON t.id = tl.ticket_id WHERE tl.machine_id = ? ORDER BY t.updated_at DESC, t.created_at DESC`, assetID, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.Ticket, 0) for rows.Next() { var ticket domain.Ticket var openedAt sql.NullTime var closedAt sql.NullTime var url sql.NullString if err := rows.Scan(&ticket.ID, &ticket.Source, &ticket.ExternalID, &ticket.Title, &ticket.Status, &openedAt, &closedAt, &url, &ticket.CreatedAt, &ticket.UpdatedAt); err != nil { return nil, err } if openedAt.Valid { value := openedAt.Time ticket.OpenedAt = &value } if closedAt.Valid { value := closedAt.Time ticket.ClosedAt = &value } if url.Valid { value := url.String ticket.URL = &value } items = append(items, ticket) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func (r *TicketRepository) ListAll(ctx context.Context, limit int) ([]domain.Ticket, error) { if limit <= 0 { limit = 200 } rows, err := r.db.QueryContext(ctx, `SELECT t.id, t.source, t.external_id, t.title, t.status, t.opened_at, t.closed_at, t.url, t.created_at, t.updated_at FROM tickets t ORDER BY t.updated_at DESC, t.created_at DESC LIMIT ?`, limit, ) if err != nil { return nil, err } defer rows.Close() items := make([]domain.Ticket, 0) for rows.Next() { var ticket domain.Ticket var openedAt sql.NullTime var closedAt sql.NullTime var url sql.NullString if err := rows.Scan(&ticket.ID, &ticket.Source, &ticket.ExternalID, &ticket.Title, &ticket.Status, &openedAt, &closedAt, &url, &ticket.CreatedAt, &ticket.UpdatedAt); err != nil { return nil, err } if openedAt.Valid { value := openedAt.Time ticket.OpenedAt = &value } if closedAt.Valid { value := closedAt.Time ticket.ClosedAt = &value } if url.Valid { value := url.String ticket.URL = &value } items = append(items, ticket) } if err := rows.Err(); err != nil { return nil, err } return items, nil }