# 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.
