From f49cbccf88e92f198261e259637389e83a2fde60 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Fri, 9 Oct 2026 08:43:03 +0200 Subject: [PATCH] fix: drop the cancelling member's waiting-list entry on reject WorkshopInvitationController#reject sets `attending: false` but leaves the member's WaitingList row in place. With the cancelling member's own entry first on the list, `WaitingList.next_spot` returns it for the seat they just freed: the flow destroys that entry, re-sets the freshly cancelled invitation to attending and emails the member they are attending again. Destroy the cancelling member's entry before looking up the next spot so the promotion goes to the real next entry for the role. --- .../workshop_invitation_controller.rb | 2 ++ .../workshop_invitation_controller_spec.rb | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/app/controllers/workshop_invitation_controller.rb b/app/controllers/workshop_invitation_controller.rb index 6b3369864..7f777b3d3 100644 --- a/app/controllers/workshop_invitation_controller.rb +++ b/app/controllers/workshop_invitation_controller.rb @@ -68,6 +68,8 @@ def reject MemberActivityRecorder.record(actor: @invitation.member, key: 'workshop_invitation.rejected', trackable: @invitation) + WaitingList.find_by(invitation_id: @invitation.id)&.destroy + next_spot = WaitingList.next_spot(@invitation.workshop, @invitation.role) if next_spot.present? diff --git a/spec/controllers/workshop_invitation_controller_spec.rb b/spec/controllers/workshop_invitation_controller_spec.rb index 21fbc136d..5133b42d8 100644 --- a/spec/controllers/workshop_invitation_controller_spec.rb +++ b/spec/controllers/workshop_invitation_controller_spec.rb @@ -170,6 +170,37 @@ expect(waitlisted_invitation.reload.attending).to be true end end + + context 'when the rejecting member is on the waiting list' do + before do + invitation.update!(attending: true) + WaitingList.add(invitation, auto_rsvp: true) + end + + it 'removes their own waiting-list entry' do + post :reject, params: { id: invitation.token } + + expect(WaitingList.where(invitation:)).to be_empty + end + + it 'does not redeliver the seat to the cancelling member' do + post :reject, params: { id: invitation.token } + + expect(invitation.reload.attending).to be false + end + + it 'promotes the next student on the waiting list instead' do + member_behind = Fabricate(:member) + invitation_behind = Fabricate(:workshop_invitation, workshop:, member: member_behind, role: 'Student') + WaitingList.add(invitation_behind, auto_rsvp: true) + + post :reject, params: { id: invitation.token } + + expect(invitation_behind.reload.attending).to be true + expect(invitation.reload.attending).to be false + expect(WaitingList.where(invitation:)).to be_empty + end + end end describe 'PATCH #update' do