Add yes/no RSVP answers with required login
RSVPs now carry a status ('yes'/'no'). When auth is enabled, answering
requires login for all events, not just capped ones. Answers are deduped
one per user and re-answering updates in place. A 'no' zeroes plus_one,
is excluded from Going counts and cap math, frees the person's slot
claims, and shows in a separate "Can't make it" list.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+93
-11
@@ -54,7 +54,7 @@ func autoRsvpPlusOne(ctx context.Context, q *db.Queries, event db.Event, name st
|
||||
})
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = q.CreateRsvp(ctx, db.CreateRsvpParams{
|
||||
EventID: event.ID, Name: name, Note: note, PlusOne: plusOne,
|
||||
EventID: event.ID, Name: name, Note: note, PlusOne: plusOne, Status: "yes",
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -162,10 +162,10 @@ func TestDeduplicateExistingRsvps(t *testing.T) {
|
||||
event := createTestEvent(t, q)
|
||||
|
||||
// Insert duplicate RSVPs directly
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Charlie", PlusOne: 0})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Charlie", PlusOne: 0})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "charlie", PlusOne: 0})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Dana", PlusOne: 0})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Charlie", PlusOne: 0, Status: "yes"})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Charlie", PlusOne: 0, Status: "yes"})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "charlie", PlusOne: 0, Status: "yes"})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Dana", PlusOne: 0, Status: "yes"})
|
||||
|
||||
rsvps, _ := q.ListRsvps(ctx, event.ID)
|
||||
if len(rsvps) != 4 {
|
||||
@@ -187,14 +187,14 @@ func TestUpdateRsvp(t *testing.T) {
|
||||
event := createTestEvent(t, q)
|
||||
|
||||
rsvp, err := q.CreateRsvp(ctx, db.CreateRsvpParams{
|
||||
EventID: event.ID, Name: "Alice", Note: "hi", PlusOne: 1,
|
||||
EventID: event.ID, Name: "Alice", Note: "hi", PlusOne: 1, Status: "yes",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = q.UpdateRsvp(ctx, db.UpdateRsvpParams{
|
||||
Name: "Alicia", Note: "updated", PlusOne: 3, ID: rsvp.ID,
|
||||
Name: "Alicia", Note: "updated", PlusOne: 3, Status: "yes", ID: rsvp.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -252,8 +252,8 @@ func TestCountGoing(t *testing.T) {
|
||||
t.Fatalf("expected 0 going on empty event, got %d", going)
|
||||
}
|
||||
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 0})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Bob", PlusOne: 2})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 0, Status: "yes"})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Bob", PlusOne: 2, Status: "yes"})
|
||||
|
||||
going, err = q.CountGoing(ctx, event.ID)
|
||||
if err != nil {
|
||||
@@ -282,7 +282,7 @@ func TestGetRsvpByUser(t *testing.T) {
|
||||
}
|
||||
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{
|
||||
EventID: event.ID, Name: "Alice", PlusOne: 1,
|
||||
EventID: event.ID, Name: "Alice", PlusOne: 1, Status: "yes",
|
||||
UserID: sql.NullInt64{Int64: user.ID, Valid: true},
|
||||
})
|
||||
|
||||
@@ -311,7 +311,7 @@ func TestAttendeeCapArithmetic(t *testing.T) {
|
||||
t.Fatalf("expected AttendeeCap=3, got %d", event.AttendeeCap)
|
||||
}
|
||||
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 1})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 1, Status: "yes"})
|
||||
|
||||
going, _ := q.CountGoing(ctx, event.ID)
|
||||
// Same check as handleRsvp: a new RSVP with plus_one=1 would exceed the cap.
|
||||
@@ -341,3 +341,85 @@ func TestRsvpPlusOne(t *testing.T) {
|
||||
t.Fatalf("expected PlusOne=2, got %d", rsvps[0].PlusOne)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRsvpNoExcludedFromCountGoing(t *testing.T) {
|
||||
_, q := setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
event := createTestEvent(t, q)
|
||||
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Alice", PlusOne: 1, Status: "yes"})
|
||||
q.CreateRsvp(ctx, db.CreateRsvpParams{EventID: event.ID, Name: "Bob", PlusOne: 0, Status: "no"})
|
||||
|
||||
going, err := q.CountGoing(ctx, event.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if going != 2 {
|
||||
t.Fatalf("expected 2 going (no's excluded), got %d", going)
|
||||
}
|
||||
|
||||
rsvps, _ := q.ListRsvps(ctx, event.ID)
|
||||
if len(rsvps) != 2 {
|
||||
t.Fatalf("expected 2 RSVPs listed (including no), got %d", len(rsvps))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwitchAnswerToNo(t *testing.T) {
|
||||
_, q := setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
event := createTestEvent(t, q)
|
||||
|
||||
rsvp, err := q.CreateRsvp(ctx, db.CreateRsvpParams{
|
||||
EventID: event.ID, Name: "Alice", PlusOne: 2, Status: "yes",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Same update handleRsvp performs when re-answering with "no".
|
||||
err = q.UpdateRsvp(ctx, db.UpdateRsvpParams{
|
||||
Name: "Alice", Note: "", PlusOne: 0, Status: "no", ID: rsvp.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
going, _ := q.CountGoing(ctx, event.ID)
|
||||
if going != 0 {
|
||||
t.Fatalf("expected 0 going after switching to no, got %d", going)
|
||||
}
|
||||
updated, _ := q.GetRsvp(ctx, rsvp.ID)
|
||||
if updated.Status != "no" {
|
||||
t.Fatalf("expected status no, got %s", updated.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoFreesClaimedSlots(t *testing.T) {
|
||||
_, q := setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
event := createTestEvent(t, q)
|
||||
|
||||
slot, err := q.CreateSlot(ctx, db.CreateSlotParams{
|
||||
EventID: event.ID, Name: "Drinks", MaxClaims: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := autoRsvp(ctx, q, event, "Alice", "seltzer", &slot.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Same cleanup handleRsvp performs on a "no" answer (case-insensitive).
|
||||
err = q.DeleteClaimsByEventAndName(ctx, db.DeleteClaimsByEventAndNameParams{
|
||||
EventID: event.ID, Name: "alice",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
count, _ := q.CountClaimsBySlot(ctx, slot.ID)
|
||||
if count != 0 {
|
||||
t.Fatalf("expected claim freed after no, got %d claims", count)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user