Refactored ticket code into kiosk code.
This commit is contained in:
parent
72b245d6d6
commit
3eec81af7f
5 changed files with 110 additions and 190 deletions
72
db.go
72
db.go
|
|
@ -176,6 +176,21 @@ func migrateV2(db *sql.DB) error {
|
||||||
addColumnIfMissing(db, "volunteers", "confirmation_token TEXT")
|
addColumnIfMissing(db, "volunteers", "confirmation_token TEXT")
|
||||||
addColumnIfMissing(db, "volunteers", "confirmed INTEGER NOT NULL DEFAULT 0")
|
addColumnIfMissing(db, "volunteers", "confirmed INTEGER NOT NULL DEFAULT 0")
|
||||||
addColumnIfMissing(db, "volunteers", "confirmed_at TEXT")
|
addColumnIfMissing(db, "volunteers", "confirmed_at TEXT")
|
||||||
|
addColumnIfMissing(db, "volunteers", "kiosk_code TEXT")
|
||||||
|
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_volunteers_kiosk_code ON volunteers(kiosk_code) WHERE kiosk_code IS NOT NULL`)
|
||||||
|
// Migrate kiosk codes from tickets to volunteers (idempotent).
|
||||||
|
db.Exec(`
|
||||||
|
UPDATE volunteers SET kiosk_code = (
|
||||||
|
SELECT t.code FROM tickets t
|
||||||
|
WHERE t.participant_id = volunteers.participant_id
|
||||||
|
AND t.code IS NOT NULL AND t.deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
) WHERE kiosk_code IS NULL AND participant_id IS NOT NULL`)
|
||||||
|
// Delete stub tickets whose code has been migrated to the volunteer.
|
||||||
|
db.Exec(`
|
||||||
|
DELETE FROM tickets
|
||||||
|
WHERE source = 'manual' AND external_id = '' AND code IS NOT NULL
|
||||||
|
AND participant_id IN (SELECT id FROM volunteers WHERE kiosk_code IS NOT NULL)`)
|
||||||
// Widen the uniqueness constraint from name-only to (name, ticket_id).
|
// Widen the uniqueness constraint from name-only to (name, ticket_id).
|
||||||
db.Exec(`DROP INDEX IF EXISTS idx_attendees_name`)
|
db.Exec(`DROP INDEX IF EXISTS idx_attendees_name`)
|
||||||
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_attendees_name_ticket ON attendees(name, ticket_id) WHERE deleted_at IS NULL`)
|
db.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_attendees_name_ticket ON attendees(name, ticket_id) WHERE deleted_at IS NULL`)
|
||||||
|
|
@ -398,6 +413,7 @@ type Volunteer struct {
|
||||||
ConfirmedAt *string `json:"confirmed_at,omitempty"`
|
ConfirmedAt *string `json:"confirmed_at,omitempty"`
|
||||||
EmailConfirmed bool `json:"email_confirmed"`
|
EmailConfirmed bool `json:"email_confirmed"`
|
||||||
ConfirmationToken *string `json:"-"`
|
ConfirmationToken *string `json:"-"`
|
||||||
|
KioskCode *string `json:"kiosk_code,omitempty"`
|
||||||
Note string `json:"note"`
|
Note string `json:"note"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
|
@ -1189,7 +1205,7 @@ const volunteerSelect = `v.id, v.participant_id, v.attendee_id,
|
||||||
COALESCE(NULLIF(p.pronouns,''), v.pronouns),
|
COALESCE(NULLIF(p.pronouns,''), v.pronouns),
|
||||||
v.department_id, v.is_lead, v.checked_in, v.checked_in_at,
|
v.department_id, v.is_lead, v.checked_in, v.checked_in_at,
|
||||||
v.confirmed, v.confirmed_at,
|
v.confirmed, v.confirmed_at,
|
||||||
v.email_confirmed, v.confirmation_token, v.note,
|
v.email_confirmed, v.confirmation_token, v.kiosk_code, v.note,
|
||||||
v.created_at, v.updated_at, v.deleted_at`
|
v.created_at, v.updated_at, v.deleted_at`
|
||||||
const volunteerFrom = `FROM volunteers v LEFT JOIN participants p ON p.id = v.participant_id`
|
const volunteerFrom = `FROM volunteers v LEFT JOIN participants p ON p.id = v.participant_id`
|
||||||
|
|
||||||
|
|
@ -1322,14 +1338,13 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
|
||||||
var v Volunteer
|
var v Volunteer
|
||||||
var participantID, attendeeID, deptID sql.NullInt64
|
var participantID, attendeeID, deptID sql.NullInt64
|
||||||
var isLead, checkedIn, confirmed, emailConfirmed int
|
var isLead, checkedIn, confirmed, emailConfirmed int
|
||||||
var confirmationToken sql.NullString
|
var confirmationToken, confirmedAt, kioskCode sql.NullString
|
||||||
var confirmedAt sql.NullString
|
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName, &v.TicketName,
|
&v.ID, &participantID, &attendeeID, &v.Name, &v.PreferredName, &v.TicketName,
|
||||||
&v.Email, &v.Phone, &v.Pronouns, &deptID,
|
&v.Email, &v.Phone, &v.Pronouns, &deptID,
|
||||||
&isLead, &checkedIn, &v.CheckedInAt,
|
&isLead, &checkedIn, &v.CheckedInAt,
|
||||||
&confirmed, &confirmedAt,
|
&confirmed, &confirmedAt,
|
||||||
&emailConfirmed, &confirmationToken, &v.Note,
|
&emailConfirmed, &confirmationToken, &kioskCode, &v.Note,
|
||||||
&v.CreatedAt, &v.UpdatedAt, &v.DeletedAt,
|
&v.CreatedAt, &v.UpdatedAt, &v.DeletedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1352,6 +1367,9 @@ func queryVolunteers(db *sql.DB, q string, args ...any) ([]Volunteer, error) {
|
||||||
if confirmedAt.Valid {
|
if confirmedAt.Valid {
|
||||||
v.ConfirmedAt = &confirmedAt.String
|
v.ConfirmedAt = &confirmedAt.String
|
||||||
}
|
}
|
||||||
|
if kioskCode.Valid {
|
||||||
|
v.KioskCode = &kioskCode.String
|
||||||
|
}
|
||||||
v.IsLead = isLead == 1
|
v.IsLead = isLead == 1
|
||||||
v.CheckedIn = checkedIn == 1
|
v.CheckedIn = checkedIn == 1
|
||||||
v.Confirmed = confirmed == 1
|
v.Confirmed = confirmed == 1
|
||||||
|
|
@ -1386,19 +1404,43 @@ func (app *App) confirmVolunteerEmail(id int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// listConfirmedVolunteersNeedingCode returns confirmed volunteers whose participant
|
func (app *App) getVolunteerByKioskCode(code string) (*Volunteer, error) {
|
||||||
// has no ticket with a code yet.
|
rows, err := queryVolunteers(app.db,
|
||||||
func (app *App) listConfirmedVolunteersNeedingCode() ([]Volunteer, error) {
|
`SELECT `+volunteerSelect+` `+volunteerFrom+` WHERE v.kiosk_code = ? AND v.deleted_at IS NULL LIMIT 1`, code)
|
||||||
|
if err != nil || len(rows) == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rows[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) assignKioskCode(id int, code string) error {
|
||||||
|
_, err := app.db.Exec(
|
||||||
|
`UPDATE volunteers SET kiosk_code=?, updated_at=? WHERE id=?`, code, now(), id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// listVolunteersNeedingKioskCode returns email-confirmed volunteers without a kiosk code.
|
||||||
|
func (app *App) listVolunteersNeedingKioskCode() ([]Volunteer, error) {
|
||||||
return queryVolunteers(app.db, `
|
return queryVolunteers(app.db, `
|
||||||
SELECT `+volunteerSelect+` `+volunteerFrom+`
|
SELECT `+volunteerSelect+` `+volunteerFrom+`
|
||||||
WHERE v.email_confirmed = 1 AND v.deleted_at IS NULL
|
WHERE v.email_confirmed = 1 AND v.kiosk_code IS NULL AND v.deleted_at IS NULL`)
|
||||||
AND v.participant_id IS NOT NULL
|
}
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1 FROM tickets t
|
func (app *App) generateVolunteerKioskCode() (string, error) {
|
||||||
WHERE t.participant_id = v.participant_id
|
for range 10 {
|
||||||
AND t.code IS NOT NULL
|
t, err := generateToken()
|
||||||
AND t.deleted_at IS NULL
|
if err != nil {
|
||||||
)`)
|
return "", err
|
||||||
|
}
|
||||||
|
var count int
|
||||||
|
if err := app.db.QueryRow(`SELECT COUNT(*) FROM volunteers WHERE kiosk_code = ?`, t).Scan(&count); err != nil {
|
||||||
|
return "", fmt.Errorf("check kiosk code uniqueness: %w", err)
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("failed to generate unique kiosk code")
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateConfirmationToken() (string, error) {
|
func generateConfirmationToken() (string, error) {
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,21 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (app *App) volunteerFromKioskToken(token string) (*Volunteer, error) {
|
||||||
|
return app.getVolunteerByKioskCode(token)
|
||||||
|
}
|
||||||
|
|
||||||
// handleKioskGet returns the volunteer's profile, current shift assignments, and
|
// handleKioskGet returns the volunteer's profile, current shift assignments, and
|
||||||
// available open shifts in their department. Authenticated by ticket code only —
|
// available open shifts in their department. Authenticated by kiosk code only —
|
||||||
// no JWT required.
|
// no JWT required.
|
||||||
func (app *App) handleKioskGet(w http.ResponseWriter, r *http.Request) {
|
func (app *App) handleKioskGet(w http.ResponseWriter, r *http.Request) {
|
||||||
token := r.PathValue("token")
|
token := r.PathValue("token")
|
||||||
t, err := app.getTicketByCode(token)
|
v, err := app.volunteerFromKioskToken(token)
|
||||||
if err != nil || t == nil {
|
if err != nil || v == nil {
|
||||||
writeError(w, "not found", http.StatusNotFound)
|
writeError(w, "not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var v *Volunteer
|
|
||||||
if t.ParticipantID != nil {
|
|
||||||
v, _ = app.getVolunteerByParticipantID(*t.ParticipantID)
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
writeError(w, "no volunteer record linked to this token", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assigned, _ := app.listShiftsForVolunteer(v.ID)
|
assigned, _ := app.listShiftsForVolunteer(v.ID)
|
||||||
if assigned == nil {
|
if assigned == nil {
|
||||||
assigned = []Shift{}
|
assigned = []Shift{}
|
||||||
|
|
@ -56,19 +51,11 @@ func (app *App) handleKioskClaim(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := app.getTicketByCode(token)
|
v, err := app.volunteerFromKioskToken(token)
|
||||||
if err != nil || t == nil {
|
if err != nil || v == nil {
|
||||||
writeError(w, "not found", http.StatusNotFound)
|
writeError(w, "not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var v *Volunteer
|
|
||||||
if t.ParticipantID != nil {
|
|
||||||
v, _ = app.getVolunteerByParticipantID(*t.ParticipantID)
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
writeError(w, "no volunteer linked to this token", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
force := r.URL.Query().Get("force") == "true"
|
force := r.URL.Query().Get("force") == "true"
|
||||||
|
|
||||||
|
|
@ -116,19 +103,11 @@ func (app *App) handleKioskUnclaim(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := app.getTicketByCode(token)
|
v, err := app.volunteerFromKioskToken(token)
|
||||||
if err != nil || t == nil {
|
if err != nil || v == nil {
|
||||||
writeError(w, "not found", http.StatusNotFound)
|
writeError(w, "not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var v *Volunteer
|
|
||||||
if t.ParticipantID != nil {
|
|
||||||
v, _ = app.getVolunteerByParticipantID(*t.ParticipantID)
|
|
||||||
}
|
|
||||||
if v == nil {
|
|
||||||
writeError(w, "no volunteer linked to this token", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := app.unassignShift(v.ID, shiftID); err != nil {
|
if err := app.unassignShift(v.ID, shiftID); err != nil {
|
||||||
writeError(w, err.Error(), http.StatusInternalServerError)
|
writeError(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,11 @@ func setupKiosk(t *testing.T) (*App, *http.ServeMux, string) {
|
||||||
dept, _ := app.createDepartment(Department{Name: "Gate"})
|
dept, _ := app.createDepartment(Department{Name: "Gate"})
|
||||||
deptID := dept.ID
|
deptID := dept.ID
|
||||||
|
|
||||||
// Create participant + ticket with code
|
// Create volunteer with a kiosk_code directly on the volunteer record
|
||||||
p, _ := app.createParticipant(Participant{Email: "titania@test.com", PreferredName: "Titania"})
|
p, _ := app.createParticipant(Participant{Email: "titania@test.com", PreferredName: "Titania"})
|
||||||
token, _ := app.generateUniqueToken()
|
v, _ := app.createVolunteer(Volunteer{Name: "Titania", ParticipantID: &p.ID, DepartmentID: &deptID})
|
||||||
tk, _ := app.createTicket(Ticket{ParticipantID: &p.ID, Name: "Titania", Source: "manual", Code: &token})
|
token, _ := app.generateVolunteerKioskCode()
|
||||||
_ = tk
|
app.assignKioskCode(v.ID, token)
|
||||||
|
|
||||||
// Create linked volunteer via participant_id
|
|
||||||
app.createVolunteer(Volunteer{Name: "Titania", ParticipantID: &p.ID, DepartmentID: &deptID})
|
|
||||||
|
|
||||||
// Create shifts
|
// Create shifts
|
||||||
app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2})
|
app.createShift(Shift{DepartmentID: deptID, Name: "Morning", Day: "2026-03-15", StartTime: "08:00", EndTime: "12:00", Capacity: 2})
|
||||||
|
|
|
||||||
104
handle_signup.go
104
handle_signup.go
|
|
@ -146,51 +146,19 @@ func (app *App) handleConfirmEmail(w http.ResponseWriter, r *http.Request) {
|
||||||
var signupsOpen string
|
var signupsOpen string
|
||||||
app.db.QueryRow(`SELECT value FROM config WHERE key = 'shift_signups_open'`).Scan(&signupsOpen)
|
app.db.QueryRow(`SELECT value FROM config WHERE key = 'shift_signups_open'`).Scan(&signupsOpen)
|
||||||
|
|
||||||
if signupsOpen == "true" && vol.ParticipantID != nil {
|
if signupsOpen == "true" {
|
||||||
// Find a ticket with a code, or create/assign one.
|
code, err := app.generateVolunteerKioskCode()
|
||||||
tickets, _ := app.listTickets(vol.ParticipantID, "")
|
if err == nil {
|
||||||
var code *string
|
if err := app.assignKioskCode(vol.ID, code); err == nil {
|
||||||
for _, tk := range tickets {
|
kioskLink := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), code)
|
||||||
if tk.Code != nil {
|
response["kiosk_link"] = kioskLink
|
||||||
code = tk.Code
|
go func() {
|
||||||
break
|
if err := app.sendShiftSignupEmail(vol.Email, vol.PreferredName, kioskLink); err != nil {
|
||||||
|
log.Printf("shift signup email to %s failed: %v", vol.Email, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if code == nil {
|
|
||||||
// No coded ticket — find any ticket or create a stub, then generate code.
|
|
||||||
var ticketID int
|
|
||||||
if len(tickets) > 0 {
|
|
||||||
ticketID = tickets[0].ID
|
|
||||||
} else {
|
|
||||||
tkName := vol.TicketName
|
|
||||||
if tkName == "" {
|
|
||||||
tkName = vol.PreferredName
|
|
||||||
}
|
|
||||||
stub, err := app.createTicket(Ticket{
|
|
||||||
ParticipantID: vol.ParticipantID,
|
|
||||||
Name: tkName,
|
|
||||||
Source: "manual",
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
ticketID = stub.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ticketID > 0 {
|
|
||||||
if t, err := app.generateUniqueToken(); err == nil {
|
|
||||||
app.db.Exec(`UPDATE tickets SET code=?, updated_at=? WHERE id=?`, t, now(), ticketID)
|
|
||||||
code = &t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if code != nil {
|
|
||||||
kioskLink := fmt.Sprintf("%s/v/%s", app.resolveBaseURL(), *code)
|
|
||||||
response["kiosk_link"] = kioskLink
|
|
||||||
go func() {
|
|
||||||
if err := app.sendShiftSignupEmail(vol.Email, vol.PreferredName, kioskLink); err != nil {
|
|
||||||
log.Printf("shift signup email to %s failed: %v", vol.Email, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, response)
|
writeJSON(w, response)
|
||||||
|
|
@ -217,62 +185,28 @@ func (app *App) handleToggleShiftSignups(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) openShiftSignups() {
|
func (app *App) openShiftSignups() {
|
||||||
// Generate codes for tickets belonging to confirmed volunteers that have no code yet.
|
// Assign kiosk codes to email-confirmed volunteers that don't have one yet.
|
||||||
vols, _ := app.listConfirmedVolunteersNeedingCode()
|
vols, _ := app.listVolunteersNeedingKioskCode()
|
||||||
for _, v := range vols {
|
for _, v := range vols {
|
||||||
if v.ParticipantID == nil {
|
code, err := app.generateVolunteerKioskCode()
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Find any ticket for this participant, or create a stub one.
|
|
||||||
tickets, _ := app.listTickets(v.ParticipantID, "")
|
|
||||||
var ticketID int
|
|
||||||
if len(tickets) > 0 {
|
|
||||||
ticketID = tickets[0].ID
|
|
||||||
} else {
|
|
||||||
tkName := v.TicketName
|
|
||||||
if tkName == "" {
|
|
||||||
tkName = v.PreferredName
|
|
||||||
}
|
|
||||||
stub, err := app.createTicket(Ticket{
|
|
||||||
ParticipantID: v.ParticipantID,
|
|
||||||
Name: tkName,
|
|
||||||
Source: "manual",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ticketID = stub.ID
|
|
||||||
}
|
|
||||||
t, err := app.generateUniqueToken()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
app.db.Exec(`UPDATE tickets SET code=?, updated_at=? WHERE id=?`, t, now(), ticketID)
|
app.assignKioskCode(v.ID, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email all confirmed volunteers that now have a ticket with a code.
|
// Email all email-confirmed volunteers that now have a kiosk code.
|
||||||
confirmed, _ := queryVolunteers(app.db, `
|
confirmed, _ := queryVolunteers(app.db, `
|
||||||
SELECT `+volunteerSelect+` `+volunteerFrom+`
|
SELECT `+volunteerSelect+` `+volunteerFrom+`
|
||||||
WHERE v.email_confirmed = 1 AND v.deleted_at IS NULL AND v.participant_id IS NOT NULL`)
|
WHERE v.email_confirmed = 1 AND v.kiosk_code IS NOT NULL AND v.deleted_at IS NULL`)
|
||||||
baseURL := app.resolveBaseURL()
|
baseURL := app.resolveBaseURL()
|
||||||
sent := 0
|
sent := 0
|
||||||
|
|
||||||
for _, v := range confirmed {
|
for _, v := range confirmed {
|
||||||
if v.ParticipantID == nil || v.Email == "" {
|
if v.Email == "" || v.KioskCode == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tickets, _ := app.listTickets(v.ParticipantID, "")
|
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *v.KioskCode)
|
||||||
var code *string
|
|
||||||
for _, tk := range tickets {
|
|
||||||
if tk.Code != nil {
|
|
||||||
code = tk.Code
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if code == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kioskLink := fmt.Sprintf("%s/v/%s", baseURL, *code)
|
|
||||||
name := v.PreferredName
|
name := v.PreferredName
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = v.Name
|
name = v.Name
|
||||||
|
|
|
||||||
|
|
@ -301,17 +301,14 @@ func TestConfirmEmailWithSignupsOpen(t *testing.T) {
|
||||||
t.Error("expected kiosk_link when signups are open")
|
t.Error("expected kiosk_link when signups are open")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ticket for participant should now have a code
|
// Volunteer should now have a kiosk_code, no stub ticket created.
|
||||||
tickets, _ := app.listTickets(&participant.ID, "")
|
vol, _ := app.getVolunteerByEmail("titania@example.com")
|
||||||
hasCode := false
|
if vol == nil || vol.KioskCode == nil {
|
||||||
for _, tk := range tickets {
|
t.Error("volunteer should have a kiosk_code after confirm with signups open")
|
||||||
if tk.Code != nil && *tk.Code != "" {
|
|
||||||
hasCode = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !hasCode {
|
tickets, _ := app.listTickets(&participant.ID, "")
|
||||||
t.Error("participant should have a ticket with code after confirm with signups open")
|
if len(tickets) != 0 {
|
||||||
|
t.Errorf("expected no stub tickets, got %d", len(tickets))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,14 +342,13 @@ func TestPublicSignupTicketNameDoesNotOverwritePreferredName(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfirmEmailStubTicketHasName(t *testing.T) {
|
func TestConfirmEmailAssignsKioskCode(t *testing.T) {
|
||||||
app := testApp(t)
|
app := testApp(t)
|
||||||
mux := testMux(app)
|
mux := testMux(app)
|
||||||
|
|
||||||
app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`)
|
app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`)
|
||||||
app.baseURL = "https://example.com"
|
app.baseURL = "https://example.com"
|
||||||
|
|
||||||
// Volunteer with a ticket_name but no pre-existing ticket
|
|
||||||
participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
|
participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
|
||||||
token := "abc123def456"
|
token := "abc123def456"
|
||||||
app.createVolunteer(Volunteer{
|
app.createVolunteer(Volunteer{
|
||||||
|
|
@ -373,47 +369,19 @@ func TestConfirmEmailStubTicketHasName(t *testing.T) {
|
||||||
if result["status"] != "confirmed" {
|
if result["status"] != "confirmed" {
|
||||||
t.Fatalf("expected confirmed, got %v", result["status"])
|
t.Fatalf("expected confirmed, got %v", result["status"])
|
||||||
}
|
}
|
||||||
|
if result["kiosk_link"] == nil {
|
||||||
|
t.Error("expected kiosk_link in response when signups are open")
|
||||||
|
}
|
||||||
|
|
||||||
// Stub ticket should have been created with TicketName as its name
|
// Kiosk code should be on the volunteer record, not a stub ticket.
|
||||||
|
vol, _ := app.getVolunteerByEmail("titania@example.com")
|
||||||
|
if vol == nil || vol.KioskCode == nil {
|
||||||
|
t.Fatal("expected volunteer to have a kiosk_code")
|
||||||
|
}
|
||||||
|
// No stub ticket should have been created.
|
||||||
tickets, _ := app.listTickets(&participant.ID, "")
|
tickets, _ := app.listTickets(&participant.ID, "")
|
||||||
if len(tickets) == 0 {
|
if len(tickets) != 0 {
|
||||||
t.Fatal("expected stub ticket to be created")
|
t.Errorf("expected no stub tickets, got %d", len(tickets))
|
||||||
}
|
|
||||||
if tickets[0].Name != "Titania Fairweather" {
|
|
||||||
t.Errorf("stub ticket name = %q, want %q", tickets[0].Name, "Titania Fairweather")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfirmEmailStubTicketFallsBackToPreferredName(t *testing.T) {
|
|
||||||
app := testApp(t)
|
|
||||||
mux := testMux(app)
|
|
||||||
|
|
||||||
app.db.Exec(`INSERT OR REPLACE INTO config (key, value) VALUES ('shift_signups_open', 'true')`)
|
|
||||||
app.baseURL = "https://example.com"
|
|
||||||
|
|
||||||
// Volunteer with no ticket_name — stub should use preferred_name
|
|
||||||
participant, _ := app.createParticipant(Participant{PreferredName: "Titania", Email: "titania@example.com"})
|
|
||||||
token := "abc123def456"
|
|
||||||
app.createVolunteer(Volunteer{
|
|
||||||
Name: "Titania",
|
|
||||||
PreferredName: "Titania",
|
|
||||||
Email: "titania@example.com",
|
|
||||||
ParticipantID: &participant.ID,
|
|
||||||
ConfirmationToken: &token,
|
|
||||||
})
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
mux.ServeHTTP(w, testRequest("POST", "/api/public/confirm", map[string]any{"token": token}))
|
|
||||||
if w.Code != 200 {
|
|
||||||
t.Fatalf("expected 200, got %d", w.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
tickets, _ := app.listTickets(&participant.ID, "")
|
|
||||||
if len(tickets) == 0 {
|
|
||||||
t.Fatal("expected stub ticket to be created")
|
|
||||||
}
|
|
||||||
if tickets[0].Name != "Titania" {
|
|
||||||
t.Errorf("stub ticket name = %q, want %q (preferred_name fallback)", tickets[0].Name, "Titania")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue