From 260e017f7964ece5f090c65f361ec0b9f4618d16 Mon Sep 17 00:00:00 2001 From: Pen Anderson Date: Wed, 4 Mar 2026 15:27:03 -0600 Subject: [PATCH] Cleaned up old Attendees model. Updated tests. --- frontend/src/api.js | 16 -- frontend/src/api.test.js | 14 +- frontend/src/db.js | 6 + frontend/src/db.test.js | 4 +- frontend/src/pages/Attendees.svelte | 274 ---------------------------- frontend/src/sync.js | 11 +- frontend/src/sync.test.js | 27 +-- handle_attendees.go | 177 ------------------ handle_attendees_test.go | 52 +++--- handle_settings.go | 11 -- handle_settings_test.go | 20 +- handle_sync.go | 5 - handle_sync_test.go | 44 ++--- main.go | 13 -- 14 files changed, 90 insertions(+), 584 deletions(-) delete mode 100644 frontend/src/pages/Attendees.svelte delete mode 100644 handle_attendees.go diff --git a/frontend/src/api.js b/frontend/src/api.js index 67d491e..b0767e6 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -72,21 +72,6 @@ export const api = { emailCode: (id) => apiJSON(`/api/tickets/${id}/email-code`, { method: 'POST' }), emailAllCodes: () => apiJSON('/api/tickets/email-codes', { method: 'POST' }), }, - attendees: { - list: (params = {}) => apiJSON('/api/attendees?' + new URLSearchParams(params)), - get: (id) => apiJSON(`/api/attendees/${id}`), - create: (data) => apiJSON('/api/attendees', { method: 'POST', body: JSON.stringify(data) }), - update: (id, data) => apiJSON(`/api/attendees/${id}`, { method: 'PUT', body: JSON.stringify(data) }), - delete: (id) => apiFetch(`/api/attendees/${id}`, { method: 'DELETE' }), - checkIn: (id, opts = {}) => - apiJSON(`/api/attendees/${id}/checkin`, { method: 'POST', body: JSON.stringify(opts) }), - generateTokens: () => - apiJSON('/api/attendees/generate-tokens', { method: 'POST' }), - emailToken: (id) => - apiJSON(`/api/attendees/${id}/email-token`, { method: 'POST' }), - emailAllTokens: () => - apiJSON('/api/attendees/email-tokens', { method: 'POST' }), - }, volunteers: { list: (params = {}) => apiJSON('/api/volunteers?' + new URLSearchParams(params)), get: (id) => apiJSON(`/api/volunteers/${id}`), @@ -126,7 +111,6 @@ export const api = { update: (data) => apiJSON('/api/settings', { method: 'PUT', body: JSON.stringify(data) }), testEmail: (to) => apiJSON('/api/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) }), toggleShiftSignups: (open) => apiJSON('/api/settings/shift-signups', { method: 'POST', body: JSON.stringify({ open }) }), - resetAttendees: () => apiJSON('/api/settings/reset-attendees', { method: 'POST' }), resetTickets: () => apiJSON('/api/settings/reset-tickets', { method: 'POST' }), resetVolunteers: () => apiJSON('/api/settings/reset-volunteers', { method: 'POST' }), resetShifts: () => apiJSON('/api/settings/reset-shifts', { method: 'POST' }), diff --git a/frontend/src/api.test.js b/frontend/src/api.test.js index f6527f5..974dd32 100644 --- a/frontend/src/api.test.js +++ b/frontend/src/api.test.js @@ -71,16 +71,16 @@ describe('api methods', () => { expect(JSON.parse(opts.body)).toEqual({ username: 'admin', password: 'pass' }) }) - it('attendees.list calls correct endpoint', async () => { - const f = mockFetch({ attendees: [] }) - await api.attendees.list({ search: 'test' }) - expect(f.mock.calls[0][0]).toBe('/api/attendees?search=test') + it('participants.list calls correct endpoint', async () => { + const f = mockFetch({ participants: [] }) + await api.participants.list({ search: 'test' }) + expect(f.mock.calls[0][0]).toBe('/api/participants?search=test') }) - it('attendees.delete uses DELETE method', async () => { + it('participants.delete uses DELETE method', async () => { const f = mockFetch({}, 204) - await api.attendees.delete(5) - expect(f.mock.calls[0][0]).toBe('/api/attendees/5') + await api.participants.delete(5) + expect(f.mock.calls[0][0]).toBe('/api/participants/5') expect(f.mock.calls[0][1].method).toBe('DELETE') }) diff --git a/frontend/src/db.js b/frontend/src/db.js index 6f45ba8..a09f332 100644 --- a/frontend/src/db.js +++ b/frontend/src/db.js @@ -40,6 +40,12 @@ db.version(3).stores({ outbox: '++id, table, op, synced_at', }) +db.version(4).stores({ + attendees: null, + outbox: null, + volunteers: 'id, name, department_id, checked_in, participant_id, deleted_at', +}) + export async function getLastSync() { const m = await db.meta.get('last_sync') return m?.value ?? '' diff --git a/frontend/src/db.test.js b/frontend/src/db.test.js index 38fc57d..282b6fc 100644 --- a/frontend/src/db.test.js +++ b/frontend/src/db.test.js @@ -9,8 +9,8 @@ describe('db schema', () => { it('has expected tables', () => { const names = db.tables.map(t => t.name).sort() expect(names).toEqual([ - 'attendees', 'departments', 'event', 'meta', - 'outbox', 'participants', 'session', 'shifts', 'tickets', 'volunteer_shifts', 'volunteers', + 'departments', 'event', 'meta', + 'participants', 'session', 'shifts', 'tickets', 'volunteer_shifts', 'volunteers', ]) }) }) diff --git a/frontend/src/pages/Attendees.svelte b/frontend/src/pages/Attendees.svelte deleted file mode 100644 index 72619b8..0000000 --- a/frontend/src/pages/Attendees.svelte +++ /dev/null @@ -1,274 +0,0 @@ - - -
- - - {#if error} -
{error}
- {/if} - {#if success} -
{success}
- {/if} - - {#if showAdd && canManage} -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - -
-
-
- {/if} - - - - {#if ($allAttendees ?? []).length === 0} -
- No attendees yet -

Import a CSV or add attendees manually.

-
- {:else} -
- - - - - - - - {#if canCheckIn}{/if} - - - - {#each filtered as a (a.id)} - - - - - - {#if canCheckIn} - - {/if} - - {/each} - -
NameTicket typeEmailStatus
- {a.name} - {#if a.ticket_id} - · {a.ticket_id} - {/if} - {#if (a.party_size ?? 1) > 1} - ×{a.party_size} - {/if} - {#if a.note} -
{a.note}
- {/if} -
{a.ticket_type || '—'} -
{a.email || '—'}
- {#if a.volunteer_token && canManage} -
- {a.volunteer_token} - {#if a.email} - - {/if} -
- {/if} -
- {#if (a.party_size ?? 1) > 1} - - {a.checked_in_count ?? 0}/{a.party_size} in - - {:else} - - {a.checked_in ? 'Checked in' : 'Pending'} - - {/if} - {#if a.checked_in_at} -
- {new Date(a.checked_in_at).toLocaleTimeString()} -
- {/if} -
- {#if (a.checked_in_count ?? 0) < (a.party_size ?? 1)} - checkIn(a)} /> - {/if} -
-
- {/if} -
diff --git a/frontend/src/sync.js b/frontend/src/sync.js index e8f2d1a..fa3b641 100644 --- a/frontend/src/sync.js +++ b/frontend/src/sync.js @@ -12,17 +12,11 @@ export async function syncPull() { const data = await api.sync.pull(since) await db.transaction('rw', - [db.event, db.attendees, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts], + [db.event, db.participants, db.tickets, db.departments, db.volunteers, db.shifts, db.volunteer_shifts], async () => { if (data.event) { await db.event.put(data.event) } - if (data.attendees?.length) { - await db.attendees.bulkPut(data.attendees) - // Purge hard-deleted records from Dexie - const deleted = data.attendees.filter(a => a.deleted_at).map(a => a.id) - if (deleted.length) await db.attendees.bulkDelete(deleted) - } if (data.participants?.length) { await db.participants.bulkPut(data.participants) const deleted = data.participants.filter(p => p.deleted_at).map(p => p.id) @@ -82,9 +76,6 @@ export function startSSE(onEvent) { try { const payload = JSON.parse(e.data) if (payload.event === 'checkin') { - if (payload.data?.type === 'attendee' && payload.data?.attendee) { - await db.attendees.put(payload.data.attendee) - } if (payload.data?.type === 'ticket' && payload.data?.ticket) { await db.tickets.put(payload.data.ticket) } diff --git a/frontend/src/sync.test.js b/frontend/src/sync.test.js index 2c8915e..21c213e 100644 --- a/frontend/src/sync.test.js +++ b/frontend/src/sync.test.js @@ -18,30 +18,31 @@ function mockFetch(body = {}, status = 200) { } describe('syncPull', () => { - it('writes attendees to Dexie', async () => { + it('writes participants to Dexie', async () => { mockFetch({ server_time: '2026-03-01T12:00:00Z', - attendees: [{ id: 1, name: 'Titania' }], + participants: [{ id: 1, preferred_name: 'Titania', email: 'titania@example.com' }], + tickets: [], departments: [], volunteers: [], shifts: [], volunteer_shifts: [], }) - // Import fresh to reset syncing guard const { syncPull } = await import('./sync.js') await syncPull() - const a = await db.attendees.get(1) - expect(a.name).toBe('Titania') + const p = await db.participants.get(1) + expect(p.preferred_name).toBe('Titania') expect(await getLastSync()).toBe('2026-03-01T12:00:00Z') }) - it('deletes soft-deleted attendees from Dexie', async () => { - await db.attendees.put({ id: 1, name: 'Titania' }) + it('deletes soft-deleted participants from Dexie', async () => { + await db.participants.put({ id: 1, preferred_name: 'Titania', email: 'titania@example.com' }) mockFetch({ server_time: '2026-03-01T13:00:00Z', - attendees: [{ id: 1, name: 'Titania', deleted_at: '2026-03-01T12:30:00Z' }], + participants: [{ id: 1, preferred_name: 'Titania', deleted_at: '2026-03-01T12:30:00Z' }], + tickets: [], departments: [], volunteers: [], shifts: [], @@ -50,8 +51,8 @@ describe('syncPull', () => { const { syncPull } = await import('./sync.js') await syncPull() - const a = await db.attendees.get(1) - expect(a).toBeUndefined() + const p = await db.participants.get(1) + expect(p).toBeUndefined() }) it('deletes soft-deleted volunteer_shifts from Dexie', async () => { @@ -59,7 +60,8 @@ describe('syncPull', () => { mockFetch({ server_time: '2026-03-01T13:00:00Z', - attendees: [], + participants: [], + tickets: [], departments: [], volunteers: [], shifts: [], @@ -75,7 +77,8 @@ describe('syncPull', () => { it('sets lastSync timestamp', async () => { mockFetch({ server_time: '2026-03-02T00:00:00Z', - attendees: [], + participants: [], + tickets: [], departments: [], volunteers: [], shifts: [], diff --git a/handle_attendees.go b/handle_attendees.go deleted file mode 100644 index 0ee5628..0000000 --- a/handle_attendees.go +++ /dev/null @@ -1,177 +0,0 @@ -package main - -import ( - "encoding/csv" - "encoding/json" - "net/http" - "strconv" -) - -func (app *App) handleListAttendees(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - attendees, err := app.listAttendees(q.Get("search"), q.Get("ticket_type"), q.Get("checked_in")) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - types, _ := app.attendeeTicketTypes() - total, checkedIn, _ := app.attendeeCounts() - writeJSON(w, map[string]any{ - "attendees": attendees, - "ticket_types": types, - "total": total, - "checked_in": checkedIn, - }) -} - -func (app *App) handleCreateAttendee(w http.ResponseWriter, r *http.Request) { - var a Attendee - if err := json.NewDecoder(r.Body).Decode(&a); err != nil { - writeError(w, "invalid request", http.StatusBadRequest) - return - } - if a.Name == "" { - writeError(w, "name is required", http.StatusBadRequest) - return - } - created, err := app.createAttendee(a) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusCreated) - writeJSON(w, created) -} - -func (app *App) handleGetAttendee(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - writeError(w, "invalid id", http.StatusBadRequest) - return - } - a, err := app.getAttendee(id) - if err != nil || a == nil { - writeError(w, "not found", http.StatusNotFound) - return - } - writeJSON(w, a) -} - -func (app *App) handleUpdateAttendee(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - writeError(w, "invalid id", http.StatusBadRequest) - return - } - var a Attendee - if err := json.NewDecoder(r.Body).Decode(&a); err != nil { - writeError(w, "invalid request", http.StatusBadRequest) - return - } - if a.Name == "" { - writeError(w, "name is required", http.StatusBadRequest) - return - } - a.ID = id - if err := app.updateAttendee(a); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - updated, _ := app.getAttendee(id) - writeJSON(w, updated) -} - -func (app *App) handleDeleteAttendee(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - writeError(w, "invalid id", http.StatusBadRequest) - return - } - if err := app.deleteAttendee(id); err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusNoContent) -} - -// handleCheckInAttendee handles POST /api/attendees/:id/checkin. -// Optional body: {"count": N, "also_volunteer": true} -// Returns {"attendee": ..., "volunteer": ...} — volunteer is included if also_volunteer=true -// and the attendee has a linked volunteer record. -func (app *App) handleCheckInAttendee(w http.ResponseWriter, r *http.Request) { - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - writeError(w, "invalid id", http.StatusBadRequest) - return - } - - var body struct { - Count int `json:"count"` - AlsoVolunteer bool `json:"also_volunteer"` - } - body.Count = 1 - json.NewDecoder(r.Body).Decode(&body) - if body.Count < 1 { - body.Count = 1 - } - - claims := claimsFromContext(r) - a, err := app.checkInAttendee(id, claims.UserID, body.Count) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - - result := map[string]any{"attendee": a} - - if body.AlsoVolunteer { - // Try to find volunteer via participant_id first (new model), fall back to attendee_id (legacy). - var v *Volunteer - if a != nil { - p, _ := app.getParticipantByEmail(a.Email) - if p != nil { - v, _ = app.getVolunteerByParticipantID(p.ID) - } - } - if v == nil { - v, _ = app.getVolunteerByAttendeeID(id) - } - if v != nil { - if !v.CheckedIn { - if v2, err := app.checkInVolunteer(v.ID, claims.UserID); err == nil { - result["volunteer"] = v2 - app.broker.publish("checkin", map[string]any{"type": "volunteer", "volunteer": v2}) - } - } else { - result["volunteer"] = v - } - } - } - - app.broker.publish("checkin", map[string]any{"type": "attendee", "attendee": a}) - writeJSON(w, result) -} - -func (app *App) handleExportAttendees(w http.ResponseWriter, r *http.Request) { - attendees, err := app.listAttendees("", "", "") - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "text/csv") - w.Header().Set("Content-Disposition", `attachment; filename="attendees.csv"`) - wr := csv.NewWriter(w) - wr.Write([]string{"name", "email", "phone", "ticket_id", "ticket_type", "party_size", "checked_in_count", "note", "checked_in"}) - for _, a := range attendees { - ci := "no" - if a.CheckedIn { - ci = "yes" - } - wr.Write([]string{ - a.Name, a.Email, a.Phone, a.TicketID, a.TicketType, - strconv.Itoa(a.PartySize), strconv.Itoa(a.CheckedInCount), - a.Note, ci, - }) - } - wr.Flush() -} diff --git a/handle_attendees_test.go b/handle_attendees_test.go index ff2a196..c5e6adb 100644 --- a/handle_attendees_test.go +++ b/handle_attendees_test.go @@ -6,14 +6,14 @@ import ( "testing" ) -func TestAttendeesListCreateDelete(t *testing.T) { +func TestParticipantsListCreateDelete(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) token := testToken(t, app, admin) mux := testMux(app) // Create - req := testAuthRequest("POST", "/api/attendees", map[string]string{"name": "Titania"}, token) + req := testAuthRequest("POST", "/api/participants", map[string]string{"preferred_name": "Titania", "email": "titania@example.com"}, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusCreated { @@ -23,20 +23,20 @@ func TestAttendeesListCreateDelete(t *testing.T) { id := created["id"].(float64) // List - req = testAuthRequest("GET", "/api/attendees", nil, token) + req = testAuthRequest("GET", "/api/participants", nil, token) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("list: status = %d", w.Code) } list := parseJSON(t, w) - attendees := list["attendees"].([]any) - if len(attendees) != 1 { - t.Errorf("list: got %d, want 1", len(attendees)) + participants := list["participants"].([]any) + if len(participants) != 1 { + t.Errorf("list: got %d, want 1", len(participants)) } // Delete - req = testAuthRequest("DELETE", "/api/attendees/"+itoa(int(id)), nil, token) + req = testAuthRequest("DELETE", "/api/participants/"+itoa(int(id)), nil, token) w = httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusNoContent { @@ -44,66 +44,66 @@ func TestAttendeesListCreateDelete(t *testing.T) { } // List again — should be empty - req = testAuthRequest("GET", "/api/attendees", nil, token) + req = testAuthRequest("GET", "/api/participants", nil, token) w = httptest.NewRecorder() mux.ServeHTTP(w, req) list = parseJSON(t, w) - if a2, ok := list["attendees"].([]any); ok && len(a2) != 0 { - t.Errorf("after delete: got %d, want 0", len(a2)) + if ps, ok := list["participants"].([]any); ok && len(ps) != 0 { + t.Errorf("after delete: got %d, want 0", len(ps)) } } -func TestCheckInAttendeeHandler(t *testing.T) { +func TestCheckInTicketHandler(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) token := testToken(t, app, admin) mux := testMux(app) - app.createAttendee(Attendee{Name: "Oberon"}) - app.db.Exec(`UPDATE attendees SET party_size = 3 WHERE id = 1`) + p, _ := app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"}) + tk, _ := app.createTicket(Ticket{ParticipantID: &p.ID, Name: "Oberon", Source: "manual"}) - // Check in 1 - req := testAuthRequest("POST", "/api/attendees/1/checkin", map[string]int{"count": 1}, token) + req := testAuthRequest("POST", "/api/tickets/"+itoa(tk.ID)+"/checkin", nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("checkin: status = %d\nbody: %s", w.Code, w.Body.String()) } result := parseJSON(t, w) - attendee := result["attendee"].(map[string]any) - if attendee["checked_in_count"] != float64(1) { - t.Errorf("checked_in_count = %v, want 1", attendee["checked_in_count"]) + ticket := result["ticket"].(map[string]any) + if ticket["checked_in_at"] == nil { + t.Error("checked_in_at should be set after check-in") } } -func TestGateRoleCanCheckIn(t *testing.T) { +func TestGatekeeperRoleCanCheckIn(t *testing.T) { app := testApp(t) gate := testUserWithRole(t, app, "gateuser", "gatekeeper", []int{}) token := testToken(t, app, gate) mux := testMux(app) - app.createAttendee(Attendee{Name: "Puck"}) + p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@example.com"}) + tk, _ := app.createTicket(Ticket{ParticipantID: &p.ID, Name: "Puck", Source: "manual"}) - req := testAuthRequest("POST", "/api/attendees/1/checkin", nil, token) + req := testAuthRequest("POST", "/api/tickets/"+itoa(tk.ID)+"/checkin", nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { - t.Errorf("gate checkin: status = %d", w.Code) + t.Errorf("gatekeeper checkin: status = %d", w.Code) } } -func TestGateRoleCannotDelete(t *testing.T) { +func TestGatekeeperRoleCannotDelete(t *testing.T) { app := testApp(t) gate := testUserWithRole(t, app, "gateuser", "gatekeeper", []int{}) token := testToken(t, app, gate) mux := testMux(app) - app.createAttendee(Attendee{Name: "Puck"}) + p, _ := app.createParticipant(Participant{PreferredName: "Puck", Email: "puck@example.com"}) - req := testAuthRequest("DELETE", "/api/attendees/1", nil, token) + req := testAuthRequest("DELETE", "/api/participants/"+itoa(p.ID), nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusForbidden { - t.Errorf("gate delete: status = %d, want 403", w.Code) + t.Errorf("gatekeeper delete: status = %d, want 403", w.Code) } } diff --git a/handle_settings.go b/handle_settings.go index f812134..d4ed01c 100644 --- a/handle_settings.go +++ b/handle_settings.go @@ -79,17 +79,6 @@ func (app *App) handleUpdateSettings(w http.ResponseWriter, r *http.Request) { app.handleGetSettings(w, r) } -func (app *App) handleResetAttendees(w http.ResponseWriter, r *http.Request) { - ts := now() - result, err := app.db.Exec(`UPDATE attendees SET deleted_at=?, updated_at=? WHERE deleted_at IS NULL`, ts, ts) - if err != nil { - writeError(w, err.Error(), http.StatusInternalServerError) - return - } - n, _ := result.RowsAffected() - writeJSON(w, map[string]any{"deleted": n}) -} - func (app *App) handleResetTickets(w http.ResponseWriter, r *http.Request) { ts := now() result, err := app.db.Exec(`UPDATE tickets SET deleted_at=?, updated_at=? WHERE deleted_at IS NULL`, ts, ts) diff --git a/handle_settings_test.go b/handle_settings_test.go index c588c88..cbc53fb 100644 --- a/handle_settings_test.go +++ b/handle_settings_test.go @@ -55,17 +55,19 @@ func TestUpdateSettings(t *testing.T) { } } -func TestResetAttendees(t *testing.T) { +func TestResetTickets(t *testing.T) { app := testApp(t) admin := testAdminUser(t, app) token := testToken(t, app, admin) mux := testMux(app) - app.createAttendee(Attendee{Name: "Titania", Email: "titania@example.com"}) - app.createAttendee(Attendee{Name: "Oberon", Email: "oberon@example.com"}) + p1, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) + p2, _ := app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"}) + app.createTicket(Ticket{ParticipantID: &p1.ID, Name: "Titania", Source: "manual"}) + app.createTicket(Ticket{ParticipantID: &p2.ID, Name: "Oberon", Source: "manual"}) w := httptest.NewRecorder() - mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-attendees", nil, token)) + mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-tickets", nil, token)) if w.Code != 200 { t.Fatalf("status = %d: %s", w.Code, w.Body.String()) @@ -75,20 +77,20 @@ func TestResetAttendees(t *testing.T) { t.Fatalf("deleted = %v, want 2", result["deleted"]) } - attendees, _ := app.listAttendees("", "", "") - if len(attendees) != 0 { - t.Fatalf("attendees remaining = %d, want 0", len(attendees)) + tickets, _ := app.listTickets(nil, "") + if len(tickets) != 0 { + t.Fatalf("tickets remaining = %d, want 0", len(tickets)) } } -func TestResetAttendeesRequiresAdmin(t *testing.T) { +func TestResetTicketsRequiresAdmin(t *testing.T) { app := testApp(t) gate := testUserWithRole(t, app, "gate1", "gatekeeper", []int{}) token := testToken(t, app, gate) mux := testMux(app) w := httptest.NewRecorder() - mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-attendees", nil, token)) + mux.ServeHTTP(w, testAuthRequest("POST", "/api/settings/reset-tickets", nil, token)) if w.Code != 403 { t.Fatalf("status = %d, want 403", w.Code) diff --git a/handle_sync.go b/handle_sync.go index a11d414..f2171ce 100644 --- a/handle_sync.go +++ b/handle_sync.go @@ -12,7 +12,6 @@ func (app *App) handleSyncPull(w http.ResponseWriter, r *http.Request) { since := r.URL.Query().Get("since") event, _ := app.getEvent() - attendees, _ := app.attendeesSince(since) participants, _ := app.listParticipants("", since) tickets, _ := app.listTickets(nil, since) departments, _ := app.listDepartments(since) @@ -20,9 +19,6 @@ func (app *App) handleSyncPull(w http.ResponseWriter, r *http.Request) { shifts, _ := app.listShifts(nil, "", since) volunteerShifts, _ := app.listVolunteerShifts(since) - if attendees == nil { - attendees = []Attendee{} - } if participants == nil { participants = []Participant{} } @@ -45,7 +41,6 @@ func (app *App) handleSyncPull(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{ "server_time": time.Now().UTC().Format("2006-01-02T15:04:05Z"), "event": event, - "attendees": attendees, "participants": participants, "tickets": tickets, "departments": departments, diff --git a/handle_sync_test.go b/handle_sync_test.go index c5d759a..e4aa2af 100644 --- a/handle_sync_test.go +++ b/handle_sync_test.go @@ -13,7 +13,7 @@ func TestSyncPullFull(t *testing.T) { token := testToken(t, app, admin) mux := testMux(app) - app.createAttendee(Attendee{Name: "Titania"}) + app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) dept, _ := app.createDepartment(Department{Name: "Gate"}) deptID := dept.ID app.createVolunteer(Volunteer{Name: "Titania", DepartmentID: &deptID}) @@ -31,9 +31,9 @@ func TestSyncPullFull(t *testing.T) { if result["server_time"] == nil { t.Error("missing server_time") } - attendees := result["attendees"].([]any) - if len(attendees) != 1 { - t.Errorf("attendees = %d, want 1", len(attendees)) + participants := result["participants"].([]any) + if len(participants) != 1 { + t.Errorf("participants = %d, want 1", len(participants)) } depts := result["departments"].([]any) if len(depts) != 1 { @@ -47,29 +47,29 @@ func TestSyncPullIncremental(t *testing.T) { token := testToken(t, app, admin) mux := testMux(app) - app.createAttendee(Attendee{Name: "Titania"}) + p1, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) // Backdate Titania so she falls before the "since" cutoff - app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE name = 'Titania'`) + app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p1.ID) since := "2026-01-01T12:00:00Z" // Oberon created with default updated_at (now), which is after our since - app.createAttendee(Attendee{Name: "Oberon"}) + app.createParticipant(Participant{PreferredName: "Oberon", Email: "oberon@example.com"}) req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) result := parseJSON(t, w) - attendees := result["attendees"].([]any) + participants := result["participants"].([]any) // Should only include Oberon (created after `since`) - if len(attendees) != 1 { - t.Errorf("incremental: got %d attendees, want 1", len(attendees)) + if len(participants) != 1 { + t.Errorf("incremental: got %d participants, want 1", len(participants)) } - if len(attendees) == 1 { - a := attendees[0].(map[string]any) - if a["name"] != "Oberon" { - t.Errorf("name = %v, want Oberon", a["name"]) + if len(participants) == 1 { + p := participants[0].(map[string]any) + if p["preferred_name"] != "Oberon" { + t.Errorf("preferred_name = %v, want Oberon", p["preferred_name"]) } } } @@ -80,31 +80,31 @@ func TestSyncPullIncludesSoftDeleted(t *testing.T) { token := testToken(t, app, admin) mux := testMux(app) - a, _ := app.createAttendee(Attendee{Name: "Titania"}) + p, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"}) // Backdate Titania's creation so the since cutoff is between creation and deletion - app.db.Exec(`UPDATE attendees SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, a.ID) + app.db.Exec(`UPDATE participants SET updated_at = '2026-01-01T00:00:00Z' WHERE id = ?`, p.ID) since := "2026-01-01T12:00:00Z" // Delete updates updated_at to now(), which is after our since - app.deleteAttendee(a.ID) + app.deleteParticipant(p.ID) req := testAuthRequest("GET", "/api/sync/pull?since="+since, nil, token) w := httptest.NewRecorder() mux.ServeHTTP(w, req) var result struct { - Attendees []struct { + Participants []struct { ID int `json:"id"` DeletedAt *string `json:"deleted_at"` - } `json:"attendees"` + } `json:"participants"` } json.Unmarshal(w.Body.Bytes(), &result) - if len(result.Attendees) != 1 { - t.Fatalf("got %d attendees, want 1", len(result.Attendees)) + if len(result.Participants) != 1 { + t.Fatalf("got %d participants, want 1", len(result.Participants)) } - if result.Attendees[0].DeletedAt == nil { + if result.Participants[0].DeletedAt == nil { t.Error("deleted_at should be set for soft-deleted record") } } diff --git a/main.go b/main.go index ee2db53..2eedcd0 100644 --- a/main.go +++ b/main.go @@ -99,18 +99,6 @@ func (app *App) registerRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/event", auth(app.handleGetEvent)) mux.HandleFunc("PUT /api/event", auth(app.handleUpdateEvent, "admin", "ticketing")) - mux.HandleFunc("GET /api/attendees", auth(app.handleListAttendees, "admin", "ticketing", "gatekeeper")) - mux.HandleFunc("POST /api/attendees", auth(app.handleCreateAttendee, "admin", "ticketing")) - mux.HandleFunc("GET /api/attendees/export", auth(app.handleExportAttendees, "admin", "ticketing")) - mux.HandleFunc("POST /api/attendees/generate-tokens", auth(app.handleGenerateTokens, "admin", "ticketing")) - mux.HandleFunc("GET /api/attendees/export-tokens", auth(app.handleExportTokenLinks, "admin", "ticketing")) - mux.HandleFunc("POST /api/attendees/email-tokens", auth(app.handleEmailAllTokens, "admin", "ticketing")) - mux.HandleFunc("GET /api/attendees/{id}", auth(app.handleGetAttendee, "admin", "ticketing", "gatekeeper")) - mux.HandleFunc("PUT /api/attendees/{id}", auth(app.handleUpdateAttendee, "admin", "ticketing")) - mux.HandleFunc("DELETE /api/attendees/{id}", auth(app.handleDeleteAttendee, "admin", "ticketing")) - mux.HandleFunc("POST /api/attendees/{id}/checkin", auth(app.handleCheckInAttendee, "admin", "ticketing", "gatekeeper")) - mux.HandleFunc("POST /api/attendees/{id}/email-token", auth(app.handleEmailToken, "admin", "ticketing")) - mux.HandleFunc("GET /api/participants", auth(app.handleListParticipants, "admin", "ticketing", "gatekeeper")) mux.HandleFunc("POST /api/participants", auth(app.handleCreateParticipant, "admin", "ticketing")) mux.HandleFunc("GET /api/participants/export", auth(app.handleExportParticipants, "admin", "ticketing")) @@ -157,7 +145,6 @@ func (app *App) registerRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/settings", auth(app.handleGetSettings, "admin", "ticketing")) mux.HandleFunc("PUT /api/settings", auth(app.handleUpdateSettings, "admin", "ticketing")) mux.HandleFunc("POST /api/settings/test-email", auth(app.handleTestEmail, "admin", "ticketing")) - mux.HandleFunc("POST /api/settings/reset-attendees", auth(app.handleResetAttendees, "admin", "ticketing")) mux.HandleFunc("POST /api/settings/reset-tickets", auth(app.handleResetTickets, "admin", "ticketing")) mux.HandleFunc("POST /api/settings/reset-volunteers", auth(app.handleResetVolunteers, "admin", "ticketing")) mux.HandleFunc("POST /api/settings/reset-shifts", auth(app.handleResetShifts, "admin", "ticketing"))