Removed dead attendees DB code.
This commit is contained in:
parent
7dbcd05262
commit
ad8c3a64b6
2 changed files with 4 additions and 415 deletions
325
db.go
325
db.go
|
|
@ -49,28 +49,6 @@ func migrate(db *sql.DB) error {
|
|||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attendees (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
ticket_id TEXT NOT NULL DEFAULT '',
|
||||
ticket_type TEXT NOT NULL DEFAULT '',
|
||||
volunteer_token TEXT UNIQUE,
|
||||
party_size INTEGER NOT NULL DEFAULT 1,
|
||||
checked_in INTEGER NOT NULL DEFAULT 0,
|
||||
checked_in_count INTEGER NOT NULL DEFAULT 0,
|
||||
checked_in_at TEXT,
|
||||
checked_in_by INTEGER REFERENCES participants(id),
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_attendees_name_ticket
|
||||
ON attendees(name, ticket_id) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS volunteers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
participant_id INTEGER NOT NULL REFERENCES participants(id) ON DELETE CASCADE,
|
||||
|
|
@ -122,6 +100,8 @@ func migrate(db *sql.DB) error {
|
|||
note TEXT NOT NULL DEFAULT '',
|
||||
email_confirmed INTEGER NOT NULL DEFAULT 0,
|
||||
confirmation_token TEXT,
|
||||
password_hash TEXT,
|
||||
login_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
deleted_at TEXT
|
||||
|
|
@ -161,95 +141,11 @@ func migrate(db *sql.DB) error {
|
|||
PRIMARY KEY (participant_id, department_id)
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := migrateAuth(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateAuth(db *sql.DB) error {
|
||||
// Add auth columns to participants (idempotent — ignore "duplicate column" errors).
|
||||
db.Exec(`ALTER TABLE participants ADD COLUMN password_hash TEXT`)
|
||||
db.Exec(`ALTER TABLE participants ADD COLUMN login_enabled INTEGER NOT NULL DEFAULT 0`)
|
||||
|
||||
// Migrate users → participants if the old users table exists.
|
||||
var hasUsers int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='users'`).Scan(&hasUsers); err != nil || hasUsers == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all users first (single connection — can't query and exec concurrently).
|
||||
type oldUser struct {
|
||||
id int
|
||||
name string
|
||||
hash string
|
||||
role string
|
||||
}
|
||||
rows, err := db.Query(`SELECT id, username, password_hash, role FROM users`)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var users []oldUser
|
||||
for rows.Next() {
|
||||
var u oldUser
|
||||
if err := rows.Scan(&u.id, &u.name, &u.hash, &u.role); err != nil {
|
||||
continue
|
||||
}
|
||||
if u.role == "ticketing" {
|
||||
u.role = "admin"
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Collect department assignments.
|
||||
type deptAssign struct {
|
||||
userID int
|
||||
deptID int
|
||||
}
|
||||
deptRows, err := db.Query(`SELECT user_id, department_id FROM user_departments`)
|
||||
var deptAssigns []deptAssign
|
||||
if err == nil {
|
||||
for deptRows.Next() {
|
||||
var da deptAssign
|
||||
deptRows.Scan(&da.userID, &da.deptID)
|
||||
deptAssigns = append(deptAssigns, da)
|
||||
}
|
||||
deptRows.Close()
|
||||
}
|
||||
|
||||
// Now insert with the connection free.
|
||||
for _, u := range users {
|
||||
res, err := db.Exec(
|
||||
`INSERT INTO participants (preferred_name, password_hash, login_enabled, updated_at) VALUES (?, ?, 1, ?)`,
|
||||
u.name, u.hash, now(),
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pid, _ := res.LastInsertId()
|
||||
db.Exec(`INSERT OR IGNORE INTO participant_roles (participant_id, role) VALUES (?, ?)`, pid, u.role)
|
||||
for _, da := range deptAssigns {
|
||||
if da.userID == u.id {
|
||||
db.Exec(`INSERT OR IGNORE INTO participant_departments (participant_id, department_id) VALUES (?, ?)`, pid, da.deptID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.Exec(`DROP TABLE IF EXISTS user_departments`)
|
||||
db.Exec(`DROP TABLE IF EXISTS users`)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Types ---
|
||||
|
||||
const attendeeCols = `id, name, email, phone, ticket_id, ticket_type, volunteer_token,
|
||||
party_size, checked_in, checked_in_count, checked_in_at, checked_in_by,
|
||||
note, created_at, updated_at, deleted_at`
|
||||
|
||||
const shiftCols = `id, department_id, name, day, start_time, end_time, capacity, position, updated_at, deleted_at`
|
||||
const shiftColsS = `s.id, s.department_id, s.name, s.day, s.start_time, s.end_time, s.capacity, s.position, s.updated_at, s.deleted_at`
|
||||
|
||||
|
|
@ -273,25 +169,6 @@ type User struct {
|
|||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type Attendee struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
TicketID string `json:"ticket_id"`
|
||||
TicketType string `json:"ticket_type"`
|
||||
VolunteerToken *string `json:"volunteer_token,omitempty"`
|
||||
PartySize int `json:"party_size"`
|
||||
CheckedIn bool `json:"checked_in"`
|
||||
CheckedInCount int `json:"checked_in_count"`
|
||||
CheckedInAt *string `json:"checked_in_at,omitempty"`
|
||||
CheckedInBy *int `json:"checked_in_by,omitempty"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
type Department struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -688,174 +565,6 @@ func (app *App) generateCodesForAll() (int, error) {
|
|||
return count, nil
|
||||
}
|
||||
|
||||
// incrementPartySize is kept for backward compatibility with existing tests.
|
||||
func (app *App) incrementPartySize(name, ticketID string) (bool, error) {
|
||||
res, err := app.db.Exec(
|
||||
`UPDATE attendees SET party_size = party_size + 1, updated_at = ?
|
||||
WHERE name = ? AND ticket_id = ? AND deleted_at IS NULL`,
|
||||
now(), name, ticketID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return n > 0, nil
|
||||
}
|
||||
|
||||
// --- Attendees ---
|
||||
|
||||
func (app *App) listAttendees(search, ticketType, checkedIn string) ([]Attendee, error) {
|
||||
q := `SELECT ` + attendeeCols + ` FROM attendees WHERE deleted_at IS NULL`
|
||||
var args []any
|
||||
if search != "" {
|
||||
q += ` AND (name LIKE ? OR email LIKE ? OR ticket_id LIKE ?)`
|
||||
s := "%" + search + "%"
|
||||
args = append(args, s, s, s)
|
||||
}
|
||||
if ticketType != "" {
|
||||
q += ` AND ticket_type = ?`
|
||||
args = append(args, ticketType)
|
||||
}
|
||||
if checkedIn == "true" {
|
||||
q += ` AND checked_in = 1`
|
||||
} else if checkedIn == "false" {
|
||||
q += ` AND checked_in = 0`
|
||||
}
|
||||
q += ` ORDER BY name ASC`
|
||||
return queryAttendees(app.db, q, args...)
|
||||
}
|
||||
|
||||
func (app *App) getAttendee(id int) (*Attendee, error) {
|
||||
rows, err := queryAttendees(app.db,
|
||||
`SELECT `+attendeeCols+` FROM attendees WHERE id = ?`, id)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) createAttendee(a Attendee) (*Attendee, error) {
|
||||
res, err := app.db.Exec(
|
||||
`INSERT INTO attendees (name, email, phone, ticket_id, ticket_type, note, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
a.Name, a.Email, a.Phone, a.TicketID, a.TicketType, a.Note, now(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
return app.getAttendee(int(id))
|
||||
}
|
||||
|
||||
func (app *App) updateAttendee(a Attendee) error {
|
||||
_, err := app.db.Exec(
|
||||
`UPDATE attendees SET name=?, email=?, phone=?, ticket_id=?, ticket_type=?, note=?, updated_at=?
|
||||
WHERE id = ? AND deleted_at IS NULL`,
|
||||
a.Name, a.Email, a.Phone, a.TicketID, a.TicketType, a.Note, now(), a.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (app *App) deleteAttendee(id int) error {
|
||||
_, err := app.db.Exec(
|
||||
`UPDATE attendees SET deleted_at = ?, updated_at = ? WHERE id = ?`, now(), now(), id,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// checkInAttendee increments checked_in_count by count (capped at party_size).
|
||||
// Sets checked_in and checked_in_at on the first check-in.
|
||||
func (app *App) checkInAttendee(id, userID, count int) (*Attendee, error) {
|
||||
if count < 1 {
|
||||
count = 1
|
||||
}
|
||||
a, err := app.getAttendee(id)
|
||||
if err != nil || a == nil {
|
||||
return nil, err
|
||||
}
|
||||
remaining := a.PartySize - a.CheckedInCount
|
||||
if count > remaining {
|
||||
count = remaining
|
||||
}
|
||||
if count <= 0 {
|
||||
return a, nil
|
||||
}
|
||||
t := now()
|
||||
_, err = app.db.Exec(`
|
||||
UPDATE attendees SET
|
||||
checked_in_count = checked_in_count + ?,
|
||||
checked_in = CASE WHEN checked_in = 0 THEN 1 ELSE checked_in END,
|
||||
checked_in_at = CASE WHEN checked_in = 0 THEN ? ELSE checked_in_at END,
|
||||
checked_in_by = CASE WHEN checked_in = 0 THEN ? ELSE checked_in_by END,
|
||||
updated_at = ?
|
||||
WHERE id = ? AND deleted_at IS NULL`,
|
||||
count, t, userID, t, id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return app.getAttendee(id)
|
||||
}
|
||||
|
||||
func (app *App) attendeesSince(since string) ([]Attendee, error) {
|
||||
return queryAttendees(app.db,
|
||||
`SELECT `+attendeeCols+` FROM attendees WHERE updated_at > ? ORDER BY updated_at ASC`, since)
|
||||
}
|
||||
|
||||
func queryAttendees(db *sql.DB, q string, args ...any) ([]Attendee, error) {
|
||||
rows, err := db.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []Attendee
|
||||
for rows.Next() {
|
||||
var a Attendee
|
||||
var checkedIn int
|
||||
var token sql.NullString
|
||||
if err := rows.Scan(
|
||||
&a.ID, &a.Name, &a.Email, &a.Phone, &a.TicketID, &a.TicketType,
|
||||
&token, &a.PartySize, &checkedIn, &a.CheckedInCount,
|
||||
&a.CheckedInAt, &a.CheckedInBy, &a.Note,
|
||||
&a.CreatedAt, &a.UpdatedAt, &a.DeletedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token.Valid && token.String != "" {
|
||||
a.VolunteerToken = &token.String
|
||||
}
|
||||
a.CheckedIn = checkedIn == 1
|
||||
if a.PartySize < 1 {
|
||||
a.PartySize = 1
|
||||
}
|
||||
result = append(result, a)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (app *App) attendeeTicketTypes() ([]string, error) {
|
||||
rows, err := app.db.Query(
|
||||
`SELECT DISTINCT ticket_type FROM attendees WHERE ticket_type != '' AND deleted_at IS NULL ORDER BY ticket_type`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var types []string
|
||||
for rows.Next() {
|
||||
var t string
|
||||
rows.Scan(&t)
|
||||
types = append(types, t)
|
||||
}
|
||||
return types, rows.Err()
|
||||
}
|
||||
|
||||
func (app *App) attendeeCounts() (total, checkedIn int, err error) {
|
||||
app.db.QueryRow(`SELECT COUNT(*) FROM attendees WHERE deleted_at IS NULL`).Scan(&total)
|
||||
app.db.QueryRow(`SELECT COUNT(*) FROM attendees WHERE checked_in=1 AND deleted_at IS NULL`).Scan(&checkedIn)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Participants ---
|
||||
|
||||
const participantCols = `id, email, preferred_name, ticket_name, phone, pronouns, note, email_confirmed, confirmation_token, created_at, updated_at, deleted_at`
|
||||
|
|
@ -1021,15 +730,6 @@ func (app *App) getTicket(id int) (*Ticket, error) {
|
|||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) getTicketByCode(code string) (*Ticket, error) {
|
||||
rows, err := queryTickets(app.db,
|
||||
`SELECT `+ticketCols+` FROM tickets WHERE code = ? AND deleted_at IS NULL`, code)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) createTicket(t Ticket) (*Ticket, error) {
|
||||
res, err := app.db.Exec(
|
||||
`INSERT INTO tickets (participant_id, name, ticket_type, source, external_id, order_id, code, updated_at)
|
||||
|
|
@ -1066,16 +766,6 @@ func (app *App) deleteTicket(id int) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (app *App) ticketsSince(since string) ([]Ticket, error) {
|
||||
return queryTickets(app.db,
|
||||
`SELECT `+ticketCols+` FROM tickets WHERE updated_at > ? ORDER BY updated_at ASC`, since)
|
||||
}
|
||||
|
||||
func (app *App) participantsSince(since string) ([]Participant, error) {
|
||||
return queryParticipants(app.db,
|
||||
`SELECT `+participantCols+` FROM participants WHERE updated_at > ? ORDER BY updated_at ASC`, since)
|
||||
}
|
||||
|
||||
func queryTickets(db *sql.DB, q string, args ...any) ([]Ticket, error) {
|
||||
rows, err := db.Query(q, args...)
|
||||
if err != nil {
|
||||
|
|
@ -1244,15 +934,6 @@ func (app *App) getVolunteer(id int) (*Volunteer, error) {
|
|||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) getVolunteerByParticipantID(participantID int) (*Volunteer, error) {
|
||||
rows, err := queryVolunteers(app.db,
|
||||
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE v.participant_id = ? AND v.deleted_at IS NULL LIMIT 1`, participantID)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &rows[0], nil
|
||||
}
|
||||
|
||||
func (app *App) createVolunteer(v Volunteer) (*Volunteer, error) {
|
||||
res, err := app.db.Exec(
|
||||
`INSERT INTO volunteers (participant_id, department_id, is_lead, note, updated_at)
|
||||
|
|
|
|||
94
db_test.go
94
db_test.go
|
|
@ -7,7 +7,7 @@ import (
|
|||
func TestMigrate(t *testing.T) {
|
||||
app := testApp(t)
|
||||
// Verify tables exist by querying each one
|
||||
tables := []string{"event", "participants", "participant_roles", "attendees", "departments", "volunteers", "shifts", "volunteer_shifts"}
|
||||
tables := []string{"event", "participants", "participant_roles", "departments", "volunteers", "shifts", "volunteer_shifts"}
|
||||
for _, table := range tables {
|
||||
var count int
|
||||
err := app.db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count)
|
||||
|
|
@ -17,98 +17,6 @@ func TestMigrate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAttendeesCRUD(t *testing.T) {
|
||||
app := testApp(t)
|
||||
|
||||
a, err := app.createAttendee(Attendee{Name: "Titania", Email: "titania@test.com", TicketType: "GA"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.ID == 0 || a.Name != "Titania" {
|
||||
t.Errorf("create: got %+v", a)
|
||||
}
|
||||
|
||||
got, err := app.getAttendee(a.ID)
|
||||
if err != nil || got == nil {
|
||||
t.Fatal("get: not found")
|
||||
}
|
||||
if got.Email != "titania@test.com" {
|
||||
t.Errorf("get: email = %q", got.Email)
|
||||
}
|
||||
|
||||
got.Name = "Titania Fairweather"
|
||||
if err := app.updateAttendee(*got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got2, _ := app.getAttendee(a.ID)
|
||||
if got2.Name != "Titania Fairweather" {
|
||||
t.Errorf("update: name = %q", got2.Name)
|
||||
}
|
||||
|
||||
if err := app.deleteAttendee(a.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// getAttendee returns soft-deleted records; listAttendees filters them
|
||||
attendees, _ := app.listAttendees("", "", "")
|
||||
for _, at := range attendees {
|
||||
if at.ID == a.ID {
|
||||
t.Error("delete: still visible in list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrementPartySize(t *testing.T) {
|
||||
app := testApp(t)
|
||||
|
||||
app.createAttendee(Attendee{Name: "Oberon", TicketID: "ORD-100"})
|
||||
|
||||
merged, err := app.incrementPartySize("Oberon", "ORD-100")
|
||||
if err != nil || !merged {
|
||||
t.Fatalf("increment: merged=%v, err=%v", merged, err)
|
||||
}
|
||||
|
||||
a, _ := app.getAttendee(1)
|
||||
if a.PartySize != 2 {
|
||||
t.Errorf("party_size = %d, want 2", a.PartySize)
|
||||
}
|
||||
|
||||
// Different ticket_id should not merge
|
||||
merged2, _ := app.incrementPartySize("Oberon", "ORD-200")
|
||||
if merged2 {
|
||||
t.Error("should not merge different ticket_id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckInAttendee(t *testing.T) {
|
||||
app := testApp(t)
|
||||
admin := testAdminUser(t, app)
|
||||
|
||||
app.createAttendee(Attendee{Name: "Puck"})
|
||||
// Set party_size directly since createAttendee defaults to 1
|
||||
app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`)
|
||||
|
||||
// Check in 1
|
||||
a, err := app.checkInAttendee(1, admin.ID, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.CheckedInCount != 1 || !a.CheckedIn {
|
||||
t.Errorf("after 1: count=%d, checked_in=%v", a.CheckedInCount, a.CheckedIn)
|
||||
}
|
||||
|
||||
// Check in 2 more (should cap at party_size=3)
|
||||
a, _ = app.checkInAttendee(1, admin.ID, 5)
|
||||
if a.CheckedInCount != 3 {
|
||||
t.Errorf("after cap: count=%d, want 3", a.CheckedInCount)
|
||||
}
|
||||
|
||||
// Check in again — already full, should stay at 3
|
||||
a, _ = app.checkInAttendee(1, admin.ID, 1)
|
||||
if a.CheckedInCount != 3 {
|
||||
t.Errorf("after full: count=%d, want 3", a.CheckedInCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateToken(t *testing.T) {
|
||||
token, err := generateToken()
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue