Allows guests to cancel their RSVP via a DELETE endpoint using their guestToken. Frontend shows cancel button in RsvpBar and clears local storage on success. Includes unit tests, integration tests, and E2E spec. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.2 KiB
Research: Cancel RSVP
Feature: 014-cancel-rsvp | Date: 2026-03-09
1. Idempotent DELETE Semantics
Decision: Return 204 No Content for both successful deletion and "already deleted" cases.
Rationale: HTTP DELETE is defined as idempotent (RFC 9110 §9.3.5). Returning 204 regardless of whether the RSVP existed simplifies client logic — the client doesn't need to distinguish "deleted now" from "was already gone." This directly satisfies FR-002 and US-3.
Alternatives considered:
- Return 404 for "not found" RSVP: Violates idempotency expectations, forces client to handle two success paths.
- Return 200 with body: Unnecessary — no useful information to return after deletion.
2. Authorization Model for Delete
Decision: DELETE requires both event token (path) and RSVP token (path). The RSVP token acts as a bearer credential — possession equals authorization.
Rationale: Consistent with the existing privacy model. The RSVP token is a UUID v4 generated server-side, unguessable. No additional auth needed. The event token scopes the operation to prevent cross-event token collision (defense in depth).
Alternatives considered:
- RSVP token only: Sufficient in practice (UUIDs are globally unique) but loses the event context in the URL, making the API less RESTful.
- Require organizer token: Would prevent guests from self-cancelling — contradicts the spec.
3. Backend Delete Implementation Pattern
Decision: Use deleteByRsvpToken(UUID) on the JPA repository. Return the count of deleted rows (0 or 1) to determine if a record was actually removed (needed for attendee count response).
Rationale: Spring Data JPA supports deleteBy... derived queries returning long (count of deleted rows). This is a single query, no need to fetch-then-delete. The existing findByRsvpToken() method confirms the naming convention.
Alternatives considered:
findByRsvpToken()thendelete(entity): Two queries instead of one. Unnecessary.- Native
@QueryDELETE: Overkill for a simple single-column delete.
4. Backend Validation: Event Token Check
Decision: Validate that the RSVP belongs to the specified event before deleting. If the RSVP token exists but belongs to a different event, return 404.
Rationale: Defense in depth. Prevents accidental or malicious cross-event RSVP deletion via URL manipulation. The combined lookup (findByEventIdAndRsvpToken) is a single indexed query.
Alternatives considered:
- Skip event validation: Simpler but allows deleting RSVPs via wrong event URLs. Minor security concern but violates principle of least surprise.
5. Frontend: Tap-to-Reveal Pattern for Cancel
Decision: The "You're attending!" bar becomes tappable. Tapping reveals a slide-out "Cancel attendance" button. Tapping outside or pressing Escape collapses it. A subtle chevron/icon hints at interactivity.
Rationale: Specified in the feature spec's design decision. Prevents accidental cancellation (two-step: reveal + confirm dialog). Keeps the default state clean and positive.
Alternatives considered:
- Always-visible cancel button: Too prominent, encourages cancellation over attendance.
- Long-press to reveal: Not discoverable, no established mobile convention for this.
- Swipe gesture: Already used for event list deletion — would create gesture ambiguity.
6. Frontend: removeRsvp vs removeEvent in localStorage
Decision: Add removeRsvp(eventToken) method to useEventStorage.ts that clears only rsvpToken and rsvpName from a stored event, keeping the event itself in the list.
Rationale: Cancel from event detail view should NOT remove the event from the list — the guest may still want to see event details. Only the RSVP fields need clearing. The existing removeEvent() method is used for event list removal (US-2).
Alternatives considered:
- Reuse
removeEvent()for both: Would remove the event from the list when cancelling from detail view — unexpected behavior.
7. Frontend: Event List Removal with RSVP
Decision: When removing an event that has an RSVP token, call DELETE on the server FIRST. On success (or 404), then remove from localStorage. On server error, show error and keep the event.
Rationale: Server-first ensures data consistency (FR-007). If we removed from localStorage first and the server call failed, the RSVP would remain on the server with no way for the guest to cancel it.
Alternatives considered:
- Optimistic removal (remove from localStorage, fire-and-forget server call): Risks data inconsistency if server call fails.
- Remove from localStorage first, retry server call: Complex retry logic, still risks inconsistency.
8. Attendee Count After Cancellation
Decision: After successful DELETE, decrement the local attendee count by 1 in the event detail view. Do not re-fetch the event.
Rationale: Avoids an extra GET request. The count is deterministic — if the delete succeeded, exactly one attendee was removed. The same pattern is used for RSVP creation (increment by 1).
Alternatives considered:
- Re-fetch event data: Extra network request, slower UX, unnecessary.
- Return updated count from DELETE endpoint: Adds response body to a 204 — semantically wrong.