# Research: Cancel Event **Feature Branch**: `016-cancel-event` | **Date**: 2026-03-12 ## Decision 1: API Endpoint Design **Decision**: Use `PATCH /events/{eventToken}` with organizer token and cancellation fields in request body. **Rationale**: - PATCH is standard REST for partial resource updates — cancellation is a state change on the event resource. - The event is not removed, so DELETE is not appropriate. The event remains visible with a cancellation banner. - The organizer token is sent in the request body to keep it out of URL/query strings and server access logs. - Request body: `{ "organizerToken": "uuid", "cancelled": true, "cancellationReason": "optional string" }`. - Response: `204 No Content` on success. - Error responses: `404` if event not found, `403` if organizer token is wrong, `409` if already cancelled. - Currently the only supported PATCH operation is cancellation. The endpoint validates that `cancelled` is `true` and rejects requests that attempt to set other fields. **Alternatives considered**: - `POST /events/{eventToken}/cancel` — rejected because a dedicated sub-resource endpoint is RPC-style, not RESTful. PATCH on the resource itself is the standard approach. - `DELETE /events/{eventToken}` — rejected because the event is not deleted, it remains visible with a cancellation banner. ## Decision 2: Database Schema Extension **Decision**: Add two columns to the `events` table: `cancelled BOOLEAN NOT NULL DEFAULT FALSE` and `cancellation_reason VARCHAR(2000)`. **Rationale**: - Boolean flag is the simplest representation of the cancelled state. - 2000 chars matches the existing description field limit — consistent and generous. - DEFAULT FALSE ensures backward compatibility with existing rows. - A Liquibase changeset (003) adds both columns. **Alternatives considered**: - Enum status field (`ACTIVE`, `CANCELLED`) — rejected as over-engineering for a binary state with no other planned transitions. - Separate cancellation table — rejected as unnecessary complexity for two columns. ## Decision 3: RSVP Blocking on Cancelled Events **Decision**: The RSVP creation endpoint (`POST /events/{eventToken}/rsvps`) checks the event's cancelled flag and returns `409 Conflict` if the event is cancelled. **Rationale**: - Server-side enforcement is required (FR-006) — frontend hiding the button is not sufficient. - 409 Conflict is semantically correct: the request conflicts with the current state of the resource. - Existing RSVPs are preserved (FR-007) — no cascade or cleanup needed. **Alternatives considered**: - 400 Bad Request — rejected because the request itself is well-formed; the conflict is with resource state. - 422 Unprocessable Entity — rejected because the issue is not validation but state conflict. ## Decision 4: Frontend Cancel Bottom Sheet **Decision**: Reuse the existing `BottomSheet.vue` component. Add cancel-specific content (textarea + confirm button) directly in `EventDetailView.vue`, similar to how the RSVP form is embedded. **Rationale**: - The spec explicitly requires the bottom sheet pattern consistent with RSVP flow (FR-002). - `BottomSheet.vue` is already a generic, accessible, glassmorphism-styled container. - No need for a separate component — the cancel form is simple (textarea + button + error message). - Error handling follows the same pattern as RSVP: inline error in the sheet, button re-enabled. **Alternatives considered**: - Separate `CancelBottomSheet.vue` component — rejected as unnecessary extraction for a simple form. - ConfirmDialog instead of BottomSheet — rejected because spec explicitly requires bottom sheet. ## Decision 5: Organizer Token Authorization **Decision**: The cancel endpoint receives the organizer token in the request body. The frontend retrieves it from localStorage via `useEventStorage.getOrganizerToken()`. **Rationale**: - Consistent with how organizer identity works throughout the app — token-based, no auth system. - The organizer token is already stored in localStorage when the event is created. - Body parameter keeps the token out of URL/query strings and server access logs. **Alternatives considered**: - Authorization header — rejected because there's no auth system; the organizer token is not a session token. - Query parameter — rejected to keep token out of server logs (same reason the attendee endpoint should eventually be migrated away from query params). ## Decision 6: GetEventResponse Extension **Decision**: Add `cancelled: boolean` and `cancellationReason: string | null` to the `GetEventResponse` schema. **Rationale**: - The frontend needs to know whether an event is cancelled to show the banner and hide RSVP buttons. - Both fields are always returned (no separate endpoint needed). - `cancelled` defaults to `false` for existing events. **Alternatives considered**: - Separate endpoint for cancellation status — rejected as unnecessary network overhead. - Only return cancellation info for cancelled events — rejected because the frontend needs the boolean regardless to decide UI state.