package store // Every query against Drizzle-owned tables (users, events, tickets, // payments) lives in this file so the read-coupling surface stays small // and auditable. Column semantics follow backend/src/db/schema.ts. import ( "context" "database/sql" "errors" "fmt" "time" ) type UserAuth struct { ID string Role string TokenVersion int AccountStatus string } func (db *DB) GetUserAuth(ctx context.Context, userID string) (UserAuth, error) { row := db.QueryRowContext(ctx, db.Rebind( "SELECT id, role, token_version, account_status FROM users WHERE id = ?"), userID) var id, role, tv, status any if err := row.Scan(&id, &role, &tv, &status); err != nil { if errors.Is(err, sql.ErrNoRows) { return UserAuth{}, ErrNotFound } return UserAuth{}, err } return UserAuth{ ID: asString(id), Role: asString(role), TokenVersion: int(asInt(tv)), AccountStatus: asString(status), }, nil } // HasPaidTicket implements the review decision: a confirmed/checked-in // ticket whose payment is 'paid', or any confirmed/checked-in ticket when // the event is free (price = 0). func (db *DB) HasPaidTicket(ctx context.Context, userID, eventID string) (bool, error) { var v any err := db.QueryRowContext(ctx, db.Rebind(` SELECT 1 FROM tickets t JOIN events e ON e.id = t.event_id LEFT JOIN payments p ON p.ticket_id = t.id WHERE t.user_id = ? AND t.event_id = ? AND t.status IN ('confirmed','checked_in') AND (p.status = 'paid' OR e.price = 0) LIMIT 1`), userID, eventID).Scan(&v) if errors.Is(err, sql.ErrNoRows) { return false, nil } return err == nil, err } type EventSummary struct { ID string Slug string Title string TitleEs string StartDatetime time.Time Status string } func (db *DB) GetEventSummary(ctx context.Context, eventID string) (EventSummary, error) { return db.getEventWhere(ctx, "id = ?", eventID) } // GetEventSummaryBySlug resolves the /events/{slug}/gallery public route. func (db *DB) GetEventSummaryBySlug(ctx context.Context, slug string) (EventSummary, error) { return db.getEventWhere(ctx, "slug = ?", slug) } func (db *DB) getEventWhere(ctx context.Context, where string, arg any) (EventSummary, error) { row := db.QueryRowContext(ctx, db.Rebind( "SELECT id, slug, title, title_es, start_datetime, status FROM events WHERE "+where), arg) var id, slug, title, titleEs, start, status any if err := row.Scan(&id, &slug, &title, &titleEs, &start, &status); err != nil { if errors.Is(err, sql.ErrNoRows) { return EventSummary{}, ErrNotFound } return EventSummary{}, err } return EventSummary{ ID: asString(id), Slug: asString(slug), Title: asString(title), TitleEs: asString(titleEs), StartDatetime: asTime(start), Status: asString(status), }, nil } // CheckMainSchema fails fast at startup if the columns this service reads // from Drizzle-owned tables have been renamed or dropped. func (db *DB) CheckMainSchema(ctx context.Context) error { checks := []string{ "SELECT id, role, token_version, account_status FROM users WHERE 1 = 0", "SELECT id, slug, title, title_es, start_datetime, status, price FROM events WHERE 1 = 0", "SELECT id, user_id, event_id, status FROM tickets WHERE 1 = 0", "SELECT id, ticket_id, status FROM payments WHERE 1 = 0", } for _, q := range checks { if _, err := db.ExecContext(ctx, q); err != nil { return fmt.Errorf("main app schema check failed (%s): %w", q, err) } } return nil }